Completa i18n IT/EN/FR/DE/ES su sito pubblico, area autenticata e admin.

Tutte le view marketing, legali, viewer, dashboard e admin usano t(); mailer utente-facing e flash localizzati; admin con LocaleResolver e selettore lingua.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 00:38:50 +02:00
co-authored by Cursor
parent fb7068f86c
commit b88f44ab1c
158 changed files with 9444 additions and 1536 deletions
@@ -10,16 +10,16 @@ module Admin
account = AdminAccount.find_by(username: params[:username]&.strip)
if account&.authenticate(params[:password])
session[:admin_account_id] = account.id
redirect_to session.delete(:admin_return_to) || admin_root_path, notice: "Accesso effettuato"
redirect_to session.delete(:admin_return_to) || admin_root_path, notice: t("admin.flash.login_success")
else
flash.now[:alert] = "Username o password non validi"
flash.now[:alert] = t("admin.flash.invalid_credentials")
render :new, status: :unauthorized
end
end
def destroy
reset_session
redirect_to admin_login_path, notice: "Disconnesso"
redirect_to admin_login_path, notice: t("admin.flash.logout_success")
end
end
end
@@ -3,20 +3,40 @@ module Admin
layout "admin"
protect_from_forgery with: :exception
before_action :set_admin_locale
before_action :require_admin_login
include ::AdminHelper
helper ApplicationHelper
helper RecordingsArchiveHelper
helper_method :current_admin_account, :admin_logged_in?
helper_method :current_admin_account, :admin_logged_in?, :current_locale, :language_options, :current_language_option
private
def set_admin_locale
I18n.locale = LocaleResolver.resolve(
cookie_jar: request.cookie_jar,
accept_language: request.get_header("HTTP_ACCEPT_LANGUAGE")
)
end
def current_locale
I18n.locale
end
def language_options
LocaleResolver.language_options
end
def current_language_option
LocaleResolver.current_language_option
end
def require_admin_login
return if admin_logged_in?
session[:admin_return_to] = request.fullpath unless request.path == admin_login_path
redirect_to admin_login_path, alert: "Accedi per continuare"
redirect_to admin_login_path, alert: t("admin.flash.login_required")
end
def current_admin_account
@@ -11,7 +11,7 @@ module Admin
payment = Billing::Payment.find(params[:payment_id])
Billing::AttachPaymentInvoice.call(payment: payment, pdf: params[:pdf])
redirect_to admin_billing_path(billing_redirect_params(payment)),
notice: "Fattura caricata e inviata a #{payment.club.billing_email}."
notice: t("admin.flash.invoice_uploaded", email: payment.club.billing_email)
rescue Billing::AttachPaymentInvoice::Error, Billing::IssueInvoice::Error => e
payment ||= Billing::Payment.find_by(id: params[:payment_id])
redirect_to admin_billing_path(payment ? billing_redirect_params(payment) : {}),
@@ -8,7 +8,7 @@ module Admin
end
def new
redirect_to admin_billing_path(club_id: @club.id), alert: "Carica il PDF dalla lista «Pagamenti da fatturare»."
redirect_to admin_billing_path(club_id: @club.id), alert: t("admin.flash.invoice_upload_hint")
end
def create
@@ -18,7 +18,7 @@ module Admin
if @invoice.save
redirect_to edit_admin_club_billing_invoice_path(@club, @invoice),
notice: "Bozza creata. Carica il PDF dalla pagina Fatturazione o qui sotto."
notice: t("admin.flash.invoice_draft_created")
else
@payment = @invoice.billing_payment
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
@@ -36,9 +36,9 @@ module Admin
if issuing?
Billing::IssueInvoice.call(invoice: @invoice, pdf: params.dig(:billing_invoice, :pdf))
redirect_to admin_billing_path(club_id: @club.id),
notice: "Fattura #{@invoice.number} emessa e inviata a #{@club.billing_email}."
notice: t("admin.flash.invoice_issued", number: @invoice.number, email: @club.billing_email)
elsif @invoice.save
redirect_to admin_billing_path(club_id: @club.id), notice: "Fattura #{@invoice.number} aggiornata."
redirect_to admin_billing_path(club_id: @club.id), notice: t("admin.flash.invoice_updated", number: @invoice.number)
else
@payment = @invoice.billing_payment
flash.now[:alert] = @invoice.errors.full_messages.join(", ")
@@ -61,7 +61,7 @@ module Admin
end
def issuing?
params[:commit].to_s == "Emetti e invia via email"
params[:commit_action].to_s == "issue"
end
def invoice_params
@@ -20,14 +20,14 @@ module Admin
reason: params[:reason],
admin: current_admin_account
)
redirect_back_or_club notice: "Abbonamento omaggio #{Plan[params[:plan_slug]].name} attivato per #{@club.name}."
redirect_back_or_club notice: t("admin.flash.comped_granted", plan: Plan[params[:plan_slug]].name, club: @club.name)
rescue Billing::AdminCompedSubscription::Error, ActiveRecord::RecordInvalid => e
redirect_back_or_club alert: e.message
end
def revoke_comped
Billing::AdminCompedSubscription.revoke(club: @club, admin: current_admin_account)
redirect_back_or_club notice: "Abbonamento omaggio revocato per #{@club.name}."
redirect_back_or_club notice: t("admin.flash.comped_revoked", club: @club.name)
rescue Billing::AdminCompedSubscription::Error => e
redirect_back_or_club alert: e.message
end
@@ -0,0 +1,11 @@
module Admin
class LocalesController < BaseController
skip_before_action :require_admin_login
def update
locale = LocaleResolver.persist!(cookies, params[:locale])
I18n.locale = locale
redirect_back fallback_location: admin_root_path
end
end
end
@@ -9,19 +9,19 @@ module Admin
def acknowledge
incident = Ops::Incident.find(params[:id])
incident.acknowledge!
redirect_to admin_ops_path, notice: "Incidente preso in carico"
redirect_to admin_ops_path, notice: t("admin.flash.ops_acknowledged")
end
def resolve
incident = Ops::Incident.find(params[:id])
incident.resolve!
redirect_to admin_ops_path, notice: "Incidente risolto"
redirect_to admin_ops_path, notice: t("admin.flash.ops_resolved")
end
def mute
incident = Ops::Incident.find(params[:id])
incident.mute!(duration: 24.hours)
redirect_to admin_ops_path, notice: "Notifiche sospese per 24 ore"
redirect_to admin_ops_path, notice: t("admin.flash.ops_muted")
end
private
@@ -5,22 +5,22 @@ module Admin
def update
unless current_admin_account.authenticate(params[:current_password])
flash.now[:alert] = "Password attuale non corretta"
flash.now[:alert] = t("admin.flash.password_current_incorrect")
return render :edit, status: :unprocessable_entity
end
if params[:password].blank? || params[:password].length < 8
flash.now[:alert] = "La nuova password deve avere almeno 8 caratteri"
flash.now[:alert] = t("admin.flash.password_too_short")
return render :edit, status: :unprocessable_entity
end
if params[:password] != params[:password_confirmation]
flash.now[:alert] = "Le password non coincidono"
flash.now[:alert] = t("admin.flash.password_mismatch")
return render :edit, status: :unprocessable_entity
end
if current_admin_account.update(password: params[:password])
redirect_to admin_root_path, notice: "Password aggiornata"
redirect_to admin_root_path, notice: t("admin.flash.password_updated")
else
flash.now[:alert] = current_admin_account.errors.full_messages.to_sentence
render :edit, status: :unprocessable_entity
@@ -12,28 +12,28 @@ module Admin
def stop
@session = StreamSession.find(params[:id])
if @session.terminal?
redirect_to admin_session_path(@session), alert: "Sessione già terminata (#{@session.status})."
redirect_to admin_session_path(@session), alert: t("admin.flash.session_already_terminated", status: @session.status)
return
end
Sessions::Stop.new(@session).call
redirect_to admin_session_path(@session), notice: "Sessione terminata."
redirect_to admin_session_path(@session), notice: t("admin.flash.session_stopped")
rescue StandardError => e
Rails.logger.error("[admin] stop session #{@session.id}: #{e.class} #{e.message}")
redirect_to admin_session_path(@session), alert: "Errore durante la chiusura: #{e.message}"
redirect_to admin_session_path(@session), alert: t("admin.flash.session_stop_error", error: e.message)
end
def regia_link
@session = StreamSession.find(params[:id])
if @session.terminal?
redirect_to admin_session_path(@session), alert: "Sessione terminata: impossibile generare il link regia."
redirect_to admin_session_path(@session), alert: t("admin.flash.session_terminated_no_regia")
return
end
access = Sessions::RegiaAccess.new(@session)
token = access.issue_token!
redirect_to admin_session_path(@session),
notice: "Link regia generato.",
notice: t("admin.flash.session_regia_generated"),
flash: {
regia_url: access.url(token),
regia_expires_at: @session.regia_token_expires_at&.iso8601
@@ -2,7 +2,7 @@ module Admin
class YoutubeController < Admin::BaseController
def platform
if ENV["YOUTUBE_CLIENT_ID"].blank?
redirect_to admin_root_path, alert: "Configura YOUTUBE_CLIENT_ID e YOUTUBE_CLIENT_SECRET in .env"
redirect_to admin_root_path, alert: t("admin.flash.youtube_not_configured")
return
end
@@ -15,9 +15,9 @@ module Public
@club,
plan: premium_checkout_return_params[:plan],
interval: premium_checkout_return_params[:interval]
), notice: "Dati salvati. Procedi con il pagamento."
), notice: t("flash.club_billing.profile_saved_proceed_payment")
else
redirect_to public_club_billing_path(@club), notice: "Dati di fatturazione aggiornati."
redirect_to public_club_billing_path(@club), notice: t("flash.club_billing.profile_updated")
end
else
flash.now[:alert] = @club.errors.full_messages.join(", ")
@@ -27,25 +27,25 @@ module Public
def cancel_subscription
unless MatchLiveTv.stripe_enabled?
redirect_to public_club_billing_path(@club), alert: "Stripe non configurato."
redirect_to public_club_billing_path(@club), alert: t("flash.club_billing.stripe_not_configured")
return
end
Billing::Stripe::CancelSubscription.call(club: @club)
sub = @club.subscription.reload
date = sub.current_period_end ? helpers.l_local(sub.current_period_end.to_date) : "fine periodo"
date = sub.current_period_end ? helpers.l_local(sub.current_period_end.to_date) : t("billing.subscription_status.end_of_period")
redirect_to public_club_billing_path(@club),
notice: "Abbonamento disdetto. Resta attivo fino al #{date}; da quel giorno passerai al piano Free."
notice: t("flash.club_billing.subscription_canceled", date: date)
rescue ArgumentError, RuntimeError => e
redirect_to public_club_billing_path(@club), alert: e.message
rescue ::Stripe::StripeError => e
redirect_to public_club_billing_path(@club), alert: "Errore Stripe: #{e.message}"
redirect_to public_club_billing_path(@club), alert: t("flash.club_billing.stripe_error", message: e.message)
end
def download_invoice
invoice = @club.billing_invoices.find(params[:invoice_id])
unless invoice.pdf.attached?
redirect_to public_club_billing_path(@club), alert: "PDF fattura non disponibile."
redirect_to public_club_billing_path(@club), alert: t("flash.club_billing.invoice_pdf_unavailable")
return
end
@@ -14,7 +14,7 @@ module Public
@selected_team = @schedulable_teams.find { |t| t.id.to_s == params[:team_id].to_s }
unless @selected_team
@match = default_match_for_form
flash.now[:alert] = "Seleziona una squadra valida."
flash.now[:alert] = t("flash.club_matches.invalid_team")
return render :new, status: :unprocessable_entity
end
@@ -22,7 +22,7 @@ module Public
match_params.merge(sport: @selected_team.sport, sets_to_win: 3)
)
redirect_to public_live_index_path(club_id: @club.id),
notice: "Partita programmata: #{@selected_team.name} vs #{@match.opponent_name}."
notice: t("flash.club_matches.scheduled", team: @selected_team.name, opponent: @match.opponent_name)
rescue ActiveRecord::RecordInvalid => e
@match = @selected_team.matches.build(match_params)
flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -44,7 +44,7 @@ module Public
return if @schedulable_teams.any?
redirect_to public_live_index_path(club_id: @club.id),
alert: "Non hai permessi per programmare partite in questa società."
alert: t("flash.club_matches.no_schedulable_teams")
end
def resolve_selected_team
@@ -22,7 +22,7 @@ module Public
return if current_user.owned_clubs.exists?(id: @club.id)
return if current_user.manageable_teams.joins(:club).exists?(clubs: { id: @club.id })
redirect_to public_clubs_path, alert: "Non autorizzato"
redirect_to public_clubs_path, alert: t("flash.club_recordings.not_authorized")
end
end
end
@@ -12,7 +12,7 @@ module Public
def create
if current_user.owned_clubs.exists?
redirect_to public_club_path(current_user.primary_club), alert: "Hai già registrato una società"
redirect_to public_club_path(current_user.primary_club), alert: t("flash.clubs.already_registered")
return
end
@@ -22,7 +22,7 @@ module Public
ClubMembership.create!(user: current_user, club: club, role: "owner")
first_team_name = params.dig(:first_team, :name).presence || "Prima squadra"
first_team_name = params.dig(:first_team, :name).presence || t("club.new.default_first_team_name")
club.teams.create!(
name: first_team_name,
sport: club.sport
@@ -34,14 +34,14 @@ module Public
if plan.in?(%w[premium_light premium_full]) && MatchLiveTv.stripe_enabled?
unless club.billing_profile_complete?
redirect_to public_club_billing_profile_path(club, plan: plan, interval: checkout_interval_param),
alert: "Completa i dati di fatturazione prima di attivare un piano premium."
alert: t("flash.clubs.complete_billing_first")
return
end
interval = checkout_interval_param
redirect_to public_club_checkout_path(club, plan: plan, interval: interval)
else
redirect_to public_club_path(club), notice: "Società e prima squadra create."
redirect_to public_club_path(club), notice: t("flash.clubs.created")
end
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -66,7 +66,7 @@ module Public
@club.assign_attributes(club_params)
attach_branding_logo(@club)
@club.save!
redirect_to public_club_path(@club), notice: "Società aggiornata."
redirect_to public_club_path(@club), notice: t("flash.clubs.updated")
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity
@@ -88,12 +88,12 @@ module Public
require_club_owner!(@club)
if @club.subscription&.admin_comped?
redirect_to public_club_billing_path(@club),
alert: "Il piano è un abbonamento omaggio gestito da Match Live TV. Per passare a un abbonamento a pagamento contatta il supporto."
alert: t("flash.clubs.comped_change_denied")
return
end
unless MatchLiveTv.stripe_enabled?
redirect_to public_club_billing_path(@club), alert: "Pagamenti non ancora configurati sul server"
redirect_to public_club_billing_path(@club), alert: t("flash.clubs.stripe_not_configured")
return
end
@@ -105,15 +105,15 @@ module Public
if sub&.stripe_subscription_id.present? && sub.active? &&
sub.plan.slug == plan_slug && sub.billing_interval == interval && !sub.plan_change_pending?
redirect_to public_club_billing_path(@club),
notice: "Sei già su #{target_plan.name} (#{Billing::Stripe::PriceCatalog.label(plan_slug: plan_slug, interval: interval)})."
notice: t("flash.clubs.already_on_plan", plan: target_plan.name, price: Billing::Stripe::PriceCatalog.label(plan_slug: plan_slug, interval: interval))
return
end
if sub&.plan_change_pending? && sub.pending_plan&.slug == plan_slug &&
sub.pending_billing_interval == interval
when_label = sub.current_period_end.present? ? I18n.l(sub.current_period_end, format: :long) : "il prossimo rinnovo"
when_label = sub.current_period_end.present? ? I18n.l(sub.current_period_end, format: :long) : t("billing.messages.next_renewal")
redirect_to public_club_billing_path(@club),
notice: "Il passaggio a #{target_plan.name} è già programmato dal #{when_label}."
notice: t("flash.clubs.change_already_scheduled", plan: target_plan.name, date: when_label)
return
end
@@ -134,7 +134,7 @@ module Public
redirect_to public_club_billing_path(@club), alert: e.message
rescue ::Stripe::StripeError => e
Rails.logger.warn("[Stripe checkout] #{e.message}")
redirect_to public_club_billing_path(@club), alert: "Errore Stripe: #{e.message}"
redirect_to public_club_billing_path(@club), alert: t("flash.clubs.stripe_error", message: e.message)
end
def youtube_connect
@@ -143,7 +143,7 @@ module Public
entitlements.assert_can_connect_youtube!
if ENV["YOUTUBE_CLIENT_ID"].blank?
redirect_to public_club_path(@club), alert: "YouTube OAuth non configurato sul server"
redirect_to public_club_path(@club), alert: t("flash.clubs.youtube_oauth_not_configured")
return
end
@@ -161,13 +161,13 @@ module Public
def youtube_disconnect
require_club_owner!(@club)
@club.youtube_credential&.destroy!
redirect_to public_club_path(@club), notice: "Canale YouTube della società scollegato"
redirect_to public_club_path(@club), notice: t("flash.clubs.youtube_disconnected")
end
def portal
require_club_owner!(@club)
unless MatchLiveTv.stripe_enabled?
redirect_to public_club_billing_path(@club), alert: "Portale pagamenti non configurato"
redirect_to public_club_billing_path(@club), alert: t("flash.clubs.portal_not_configured")
return
end
@@ -204,7 +204,7 @@ module Public
@club,
plan: plan_slug,
interval: params[:interval].presence
), alert: "Completa i dati di fatturazione prima di attivare un piano premium."
), alert: t("flash.clubs.complete_billing_first")
end
def checkout_interval_param
@@ -236,10 +236,10 @@ module Public
def apply_checkout_flash!
case params[:checkout]
when "success"
plan_name = @subscription&.plan&.name || "premium"
flash.now[:notice] = "Pagamento completato! Piano attivo: #{plan_name}."
plan_name = @subscription&.plan&.name || "Premium"
flash.now[:notice] = t("flash.clubs.checkout_success", plan: plan_name)
when "canceled"
flash.now[:alert] = "Pagamento annullato. Nessun addebito è stato effettuato."
flash.now[:alert] = t("flash.clubs.checkout_canceled")
end
end
end
@@ -4,24 +4,24 @@ module Public
@token = params[:token]
@invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(@token.to_s))
unless @invitation
redirect_to public_pricing_path, alert: "Invito non valido o scaduto"
redirect_to public_pricing_path, alert: t("flash.invitations.invalid_or_expired")
end
end
def accept
invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(params[:token].to_s))
return redirect_to public_pricing_path, alert: "Invito non valido" unless invitation
return redirect_to public_pricing_path, alert: t("flash.invitations.invalid") unless invitation
if logged_in?
if current_user.email.downcase != invitation.email.downcase
redirect_to public_pricing_path, alert: "Questo invito è per #{invitation.email}"
redirect_to public_pricing_path, alert: t("flash.invitations.wrong_email", email: invitation.email)
return
end
invitation.accept!(current_user)
redirect_to public_team_details_path(invitation.team), notice: "Sei entrato nella squadra!"
redirect_to public_team_details_path(invitation.team), notice: t("flash.invitations.joined_team")
else
session[:pending_invite_token] = params[:token]
redirect_to public_signup_path, notice: "Registrati con #{invitation.email} per accettare l'invito"
redirect_to public_signup_path, notice: t("flash.invitations.signup_to_accept", email: invitation.email)
end
end
end
@@ -25,7 +25,7 @@ module Public
match_params.merge(sport: @team.sport, sets_to_win: match_params[:sets_to_win].presence || 3)
)
redirect_to public_team_matches_path(@team),
notice: "Partita programmata: #{@match.opponent_name}. Avvia lo streaming dallapp quando sei in palestra."
notice: t("flash.matches.scheduled", opponent: @match.opponent_name)
rescue ActiveRecord::RecordInvalid => e
@match = @team.matches.build(match_params)
flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -37,7 +37,7 @@ module Public
def update
@match.update!(match_params)
redirect_to public_team_matches_path(@team), notice: "Partita aggiornata."
redirect_to public_team_matches_path(@team), notice: t("flash.matches.updated")
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity
@@ -47,12 +47,12 @@ module Public
active = active_session_for(@match)
if active&.resumable?
redirect_to public_team_matches_path(@team),
alert: "Chiudi la diretta prima di eliminare questa partita."
alert: t("flash.matches.close_live_before_delete")
return
end
@match.destroy!
redirect_to public_team_matches_path(@team), notice: "Partita eliminata."
redirect_to public_team_matches_path(@team), notice: t("flash.matches.deleted")
end
private
@@ -66,7 +66,7 @@ module Public
return if current_user.can_schedule_for?(@team)
redirect_to public_team_details_path(@team),
alert: "Solo un responsabile trasmissione o il titolare della società possono programmare le partite."
alert: t("flash.matches.schedule_denied")
end
def set_match
@@ -11,14 +11,14 @@ module Public
end
redirect_to public_login_path,
notice: "Se l'email è registrata, riceverai a breve un link per reimpostare la password."
notice: t("flash.password_resets.email_sent")
end
def edit
@user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path,
alert: "Link non valido o scaduto. Richiedi un nuovo reset password."
alert: t("flash.password_resets.invalid_or_expired_link")
return
end
@token = params[:token]
@@ -28,25 +28,25 @@ module Public
@user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path,
alert: "Link non valido o scaduto. Richiedi un nuovo reset password."
alert: t("flash.password_resets.invalid_or_expired_link")
return
end
if params[:password].blank? || params[:password].length < 8
flash.now[:alert] = "La password deve avere almeno 8 caratteri"
flash.now[:alert] = t("flash.password_resets.password_min_length")
@token = params[:token]
return render :edit, status: :unprocessable_entity
end
if params[:password] != params[:password_confirmation]
flash.now[:alert] = "Le password non coincidono"
flash.now[:alert] = t("flash.password_resets.password_mismatch")
@token = params[:token]
return render :edit, status: :unprocessable_entity
end
@user.update!(password: params[:password])
@user.clear_password_reset!
redirect_to public_login_path, notice: "Password aggiornata. Ora puoi accedere."
redirect_to public_login_path, notice: t("flash.password_resets.password_updated")
end
end
end
@@ -8,7 +8,7 @@ module Public
def create
unless params[:accept_terms] == "1"
@user = User.new(user_params.merge(role: "coach"))
flash.now[:alert] = "Devi accettare linformativa privacy e i termini di servizio per registrarti."
flash.now[:alert] = t("flash.registrations.accept_terms_required")
return render :new, status: :unprocessable_entity
end
@@ -20,10 +20,10 @@ module Public
invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(token))
if invitation && invitation.email.downcase == @user.email.downcase
invitation.accept!(@user)
return redirect_to public_team_details_path(invitation.team), notice: "Benvenuto nella squadra!"
return redirect_to public_team_details_path(invitation.team), notice: t("flash.registrations.welcome_to_team")
end
end
redirect_to public_new_club_path, notice: "Account creato. Ora registra la tua società."
redirect_to public_new_club_path, notice: t("flash.registrations.account_created")
else
flash.now[:alert] = @user.errors.full_messages.join(", ")
render :new, status: :unprocessable_entity
@@ -42,7 +42,7 @@ module Public
def download
unless download_allowed?
redirect_to public_replay_path(@session), alert: "Download non disponibile"
redirect_to public_replay_path(@session), alert: t("flash.replay.download_unavailable")
return
end
@@ -105,13 +105,13 @@ module Public
@recording = Recording.not_deleted.find_by(stream_session: @session)
return if @recording
redirect_to public_live_path(@session), alert: "Replay non disponibile"
redirect_to public_live_path(@session), alert: t("flash.replay.not_available")
end
def authorize_replay_access!
return if Recordings::AccessPolicy.new(@recording, viewer: current_user).allowed?
redirect_to public_live_index_path, alert: "Replay non disponibile"
redirect_to public_live_index_path, alert: t("flash.replay.not_available")
end
end
end
@@ -17,16 +17,16 @@ module Public
else
public_new_club_path
end
redirect_to dest, notice: "Bentornato!"
redirect_to dest, notice: t("flash.sessions.welcome_back")
else
flash.now[:alert] = "Email o password non validi"
flash.now[:alert] = t("flash.sessions.invalid_credentials")
render :new, status: :unauthorized
end
end
def destroy
reset_session
redirect_to public_pricing_path, notice: "Disconnesso"
redirect_to public_pricing_path, notice: t("flash.sessions.logged_out")
end
end
end
@@ -9,7 +9,7 @@ module Public
member = @team.roster_members.build(member_params)
attach_photo(member)
member.save!
redirect_to public_team_details_path(@team), notice: "#{member.full_name} aggiunto all'organico."
redirect_to public_team_details_path(@team), notice: t("flash.team_roster_members.added", name: member.full_name)
rescue ActiveRecord::RecordInvalid => e
redirect_to public_team_details_path(@team), alert: e.record.errors.full_messages.join(", ")
end
@@ -22,7 +22,7 @@ module Public
@member.assign_attributes(member_params)
attach_photo(@member)
@member.save!
redirect_to public_team_details_path(@team), notice: "Scheda aggiornata."
redirect_to public_team_details_path(@team), notice: t("flash.team_roster_members.updated")
rescue ActiveRecord::RecordInvalid => e
@club = @team.club
flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -32,7 +32,7 @@ module Public
def destroy
name = @member.full_name
@member.destroy!
redirect_to public_team_details_path(@team), notice: "#{name} rimosso dall'organico."
redirect_to public_team_details_path(@team), notice: t("flash.team_roster_members.removed", name: name)
end
private
@@ -19,7 +19,7 @@ module Public
require_club_owner!(@club)
team = @club.teams.create!(team_params)
attach_branding_logo(team)
redirect_to public_club_path(@club), notice: "Squadra «#{team.name}» aggiunta."
redirect_to public_club_path(@club), notice: t("flash.teams.added", name: team.name)
rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :new, status: :unprocessable_entity
@@ -49,7 +49,7 @@ module Public
attach_branding_logo(@team)
attach_team_photo(@team)
@team.save!
redirect_to public_team_details_path(@team), notice: "Squadra aggiornata."
redirect_to public_team_details_path(@team), notice: t("flash.teams.updated")
rescue ActiveRecord::RecordInvalid => e
@club = @team.club
flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -66,7 +66,7 @@ module Public
membership = current_user.user_teams.find_or_initialize_by(team: @team)
membership.role = "member" if membership.new_record?
Teams::StaffAssignment.call(team: @team, user: current_user, membership: membership)
redirect_to public_team_details_path(@team), notice: "Il tuo account è ora responsabile trasmissione."
redirect_to public_team_details_path(@team), notice: t("flash.teams.now_staff")
rescue Teams::StaffAssignmentError, Teams::EntitlementError => e
redirect_to public_team_details_path(@team), alert: e.message
end
@@ -75,7 +75,7 @@ module Public
require_club_owner_for_team!(@team)
membership = current_user.user_teams.find_by(team: @team)
membership&.update!(staff_kind: nil)
redirect_to public_team_details_path(@team), notice: "Ruolo staff rimosso dal tuo account."
redirect_to public_team_details_path(@team), notice: t("flash.teams.staff_role_removed")
end
def create_invitation
@@ -95,7 +95,7 @@ module Public
expires_at: 7.days.from_now
)
@invite_url = join_public_invitation_url(token: token)
flash.now[:notice] = "Link invito generato (valido 7 giorni)"
flash.now[:notice] = t("flash.teams.invite_link_generated")
render :invite
rescue Teams::EntitlementError, Teams::StaffAssignmentError => e
redirect_to public_team_invite_path(@team), alert: e.message
@@ -105,14 +105,14 @@ module Public
require_club_owner_for_team!(@team)
ut = @team.user_teams.find_by!(user_id: params[:user_id], role: "member")
ut.destroy!
redirect_to public_team_details_path(@team), notice: "Accesso revocato"
redirect_to public_team_details_path(@team), notice: t("flash.teams.access_revoked")
end
def destroy_invitation
require_club_owner_for_team!(@team)
inv = @team.team_invitations.pending.find(params[:invitation_id])
inv.destroy!
redirect_to public_team_details_path(@team), notice: "Invito annullato"
redirect_to public_team_details_path(@team), notice: t("flash.teams.invitation_canceled")
end
def youtube_connect
@@ -125,7 +125,7 @@ module Public
def youtube_disconnect
require_club_owner_for_team!(@team)
@team.club.youtube_credential&.destroy!
redirect_to public_club_path(@team.club), notice: "Canale YouTube della società scollegato"
redirect_to public_club_path(@team.club), notice: t("flash.teams.youtube_disconnected")
end
private
@@ -7,15 +7,15 @@ module Public
def require_login!
return if logged_in?
redirect_to public_login_path, alert: "Accedi per continuare"
redirect_to public_login_path, alert: t("flash.web_base.login_required")
end
def require_club_owner!(club)
redirect_to public_prezzi_path, alert: "Accesso non consentito" unless club.owned_by?(current_user)
redirect_to public_prezzi_path, alert: t("flash.web_base.access_denied") unless club.owned_by?(current_user)
end
def require_club_owner_for_team!(team)
redirect_to public_prezzi_path, alert: "Accesso non consentito" unless team.club&.owned_by?(current_user)
redirect_to public_prezzi_path, alert: t("flash.web_base.access_denied") unless team.club&.owned_by?(current_user)
end
end
end
+1 -1
View File
@@ -23,7 +23,7 @@ module AdminHelper
def admin_session_watch_links(session)
links = []
if session.matchlivetv_platform?
links << { label: "Pagina live", url: session.watch_page_url }
links << { label: I18n.t("admin.common.live_page"), url: session.watch_page_url }
end
youtube = session.youtube_watch_url
links << { label: "YouTube", url: youtube } if youtube.present?
+28
View File
@@ -31,6 +31,34 @@ module ApplicationHelper
I18n.locale.to_s
end
def confirm_dialog_i18n_json
{
eyebrow: t("common.confirm_eyebrow"),
title: t("common.confirm_title"),
deleteTitle: t("common.delete_title"),
confirmOk: t("common.confirm_ok"),
deleteOk: t("common.delete_ok"),
cancel: t("common.cancel"),
multiDeleteTitle: t("common.multi_delete_title"),
multiDeleteMessage: t("common.multi_delete_message"),
deleteYoutubeTitle: t("common.delete_youtube_title"),
deleteYoutubeMessage: t("common.delete_youtube_message"),
deleteYoutubeOk: t("common.delete_youtube_ok"),
deleteSiteTitle: t("common.delete_site_title"),
deleteSiteMessageWithYoutube: t("common.delete_site_message_with_youtube"),
deleteSiteMessageOnly: t("common.delete_site_message_only"),
deleteSiteOk: t("common.delete_site_ok"),
choiceSiteLabel: t("common.choice_site_label"),
choiceSiteHintWithYoutube: t("common.choice_site_hint_with_youtube"),
choiceSiteHintOnly: t("common.choice_site_hint_only"),
choiceYoutubeLabel: t("common.choice_youtube_label"),
choiceYoutubeHintWithSite: t("common.choice_youtube_hint_with_site"),
choiceYoutubeHintOnly: t("common.choice_youtube_hint_only"),
choiceBothLabel: t("common.choice_both_label"),
choiceBothHint: t("common.choice_both_hint")
}.to_json
end
def sport_catalog_options(selected = nil)
options = Sports::Catalog.as_api_list.map { |entry| [entry[:label], entry[:key]] }
options_for_select(options, Sports::Catalog.normalize_key(selected))
+7 -7
View File
@@ -2,25 +2,25 @@ module Public
module BillingHelper
def plan_billing_action(current_slug:, target_plan:, stripe_subscription_active:, current_interval: nil, subscription: nil)
if target_plan.slug == "free"
return { kind: :current, label: "Piano attuale" } if current_slug == "free"
return { kind: :current, label: I18n.t("billing.actions.current_plan") } if current_slug == "free"
return { kind: :none } if stripe_subscription_active
return { kind: :contact, label: "Per passare al piano Free, contatta il supporto." }
return { kind: :contact, label: I18n.t("billing.actions.contact_for_free") }
end
unless MatchLiveTv.stripe_enabled?
return { kind: :disabled, label: "Stripe non configurato" }
return { kind: :disabled, label: I18n.t("billing.actions.stripe_not_configured") }
end
intervals = Billing::Stripe::PriceCatalog.available_intervals(plan_slug: target_plan.slug)
return { kind: :disabled, label: "Prezzi Stripe non configurati" } if intervals.empty?
return { kind: :disabled, label: I18n.t("billing.actions.stripe_prices_not_configured") } if intervals.empty?
active_interval = current_interval.presence || Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL
if current_slug == target_plan.slug && stripe_subscription_active && !subscription&.plan_change_pending?
other_intervals = intervals - [active_interval]
if other_intervals.empty?
label = "Piano attuale#{Billing::Stripe::PriceCatalog.label(plan_slug: target_plan.slug, interval: active_interval)}"
label = "#{I18n.t('billing.actions.current_plan')}#{Billing::Stripe::PriceCatalog.label(plan_slug: target_plan.slug, interval: active_interval)}"
return { kind: :current, label: label }
end
@@ -87,9 +87,9 @@ module Public
def billing_profile_incomplete_message(club)
missing = club.billing_profile_errors
return "Completa i dati di fatturazione prima di attivare un piano premium." if missing.empty?
return I18n.t("billing.actions.profile_incomplete_default") if missing.empty?
"Mancano: #{missing.join(", ")}."
I18n.t("billing.actions.profile_missing", fields: missing.join(", "))
end
end
+16 -13
View File
@@ -8,16 +8,16 @@ module Public
when "basket", "timed"
live_score_period_label(match, score_state)
when "timer"
"Cronometro"
t("score.timer_label")
when "generic"
"Punteggio"
t("score.generic_label")
else
"Set #{score_state.current_set} · Set vinti #{score_state.home_sets}-#{score_state.away_sets}"
t("score.sets_label", set: score_state.current_set, home: score_state.home_sets, away: score_state.away_sets)
end
end
def live_score_period_label(match, score_state)
return "" unless score_state
return t("score.fallback") unless score_state
score_state.current_period_label
end
@@ -35,16 +35,16 @@ module Public
def live_scheduled_label(datetime)
return nil unless datetime
datetime.in_time_zone.strftime("%d/%m/%Y alle %H:%M")
datetime.in_time_zone.strftime(t("score.scheduled_date_format"))
end
def live_scheduled_relative(datetime)
return nil unless datetime
if datetime.to_date == Time.zone.today
"Oggi alle #{datetime.strftime('%H:%M')}"
t("score.scheduled_today", time: datetime.strftime("%H:%M"))
elsif datetime.to_date == Time.zone.tomorrow
"Domani alle #{datetime.strftime('%H:%M')}"
t("score.scheduled_tomorrow", time: datetime.strftime("%H:%M"))
else
live_scheduled_label(datetime)
end
@@ -52,7 +52,7 @@ module Public
def live_match_card_heading(match, link_team: true)
team = match.team
club_name = team.club&.name.presence || "Società"
club_name = team.club&.name.presence || t("score.default_club_name")
team_slug = team.respond_to?(:slug) ? team.slug : nil
team_label = if link_team && team_slug.present?
link_to(team.name, public_team_page_path(team_slug), class: "live-card__team-link")
@@ -69,7 +69,7 @@ module Public
def live_match_page_heading(match)
team = match.team
club_name = team.club&.name.presence || "Società"
club_name = team.club&.name.presence || t("score.default_club_name")
content_tag(:div, class: "live-page-heading") do
safe_join([
content_tag(:p, club_name, class: "live-page-heading__club"),
@@ -79,7 +79,7 @@ module Public
end
def live_score_points_label(match, score_state)
return "" unless score_state
return t("score.fallback") unless score_state
board = match.effective_board_type
home = case board
@@ -91,7 +91,7 @@ module Public
else score_state.away_points
end
"#{match.team.name} #{home} - #{away} #{match.opponent_name}"
t("score.points_label", team: match.team.name, home: home, away: away, opponent: match.opponent_name)
end
private
@@ -101,8 +101,11 @@ module Public
set_no = data[:set] || data["set"]
home = data[:home] || data["home"]
away = data[:away] || data["away"]
label = set_no.present? ? "Set #{set_no}" : "Set"
"#{label} #{home}-#{away}"
if set_no.present?
t("score.partial_entry", set: set_no, home: home, away: away)
else
t("score.partial_entry_no_number", home: home, away: away)
end
end
end
end
+49 -3
View File
@@ -12,14 +12,60 @@ module Public
when "basket", "timed"
live_score_period_label(match, score)
when "timer"
"Cronometro"
t("regia.board.stopwatch")
when "generic"
"Punteggio"
t("regia.board.generic_score")
else
"Set #{score.current_set}"
t("regia.board.set_label", number: score.current_set)
end
end
def regia_js_i18n_json
{
periodUpdated: t("regia.js.period_updated"),
nowInPeriodTemplate: t("regia.js.now_in_period", period: "%{period}"),
setWonTitle: t("regia.modal.set_won_title"),
setWonBodyTemplate: t("regia.js.set_won_body", winner: "%{winner}"),
matchWonTitle: t("regia.js.match_won_title"),
matchWonBodyTemplate: t("regia.js.match_won_body", winner: "%{winner}"),
closeSetTitle: t("regia.board.close_set"),
closeSetConfirm: t("regia.js.close_set_confirm"),
scoreUpdateError: t("regia.js.score_update_error"),
genericError: t("regia.js.generic_error"),
setsLineTemplate: t("regia.js.sets_line", current: "%{current}", home: "%{home}", away: "%{away}"),
setPartialLabelTemplate: t("regia.board.set_label", number: "%{number}"),
freeScore: t("regia.board.free_score"),
stopwatch: t("regia.board.stopwatch"),
quarterLabelTemplate: t("regia.js.quarter_label", n: "%{n}"),
halfLabelTemplate: t("regia.js.half_label", n: "%{n}"),
overtimeBasketTemplate: t("regia.js.overtime_basket", n: "%{n}"),
overtimeOther: t("regia.js.overtime_other"),
unknownPeriod: t("regia.js.unknown_period"),
partialsPrefix: t("regia.board.partials_prefix"),
resumed: t("regia.js.resumed"),
pausedCover: t("regia.js.paused_cover"),
closed: t("regia.js.closed"),
resumeError: t("regia.js.resume_error"),
pauseError: t("regia.js.pause_error"),
closeError: t("regia.js.close_error"),
closeConfirm: t("regia.js.close_confirm"),
linkUnavailable: t("regia.js.link_unavailable"),
linkCopied: t("regia.js.link_copied"),
copyPrompt: t("regia.js.copy_prompt"),
shareRegiaText: t("regia.js.share_regia_text"),
shareLiveText: t("regia.js.share_live_text"),
previewWaiting: t("regia.preview_waiting"),
streamEnded: t("regia.preview_ended"),
resumeLabel: t("regia.resume"),
pauseLabel: t("regia.pause"),
endedBadge: t("regia.status.ended"),
pausedBadge: t("regia.status.paused"),
liveBadge: t("regia.status.live"),
waitingBadge: t("regia.status.waiting"),
subtitlePrefix: t("regia.subtitle_prefix")
}.to_json
end
def format_clock_secs(secs, count_up: false)
total = secs.to_i
return count_up ? "0:00" : "" if total <= 0 && !count_up
@@ -1,7 +1,7 @@
module Public
module TeamPagesHelper
def team_page_meta_description(team, club)
"Segui #{team.name} (#{club.name}): dirette live, calendario partite e replay su Match Live TV."
t("team_pages.show.meta_description", team: team.name, club: club.name)
end
def filter_params_for_canonical
+2 -2
View File
@@ -19,14 +19,14 @@ module RosterHelper
def roster_category_options
TeamRosterMember::DISPLAY_ORDER.map do |cat|
[TeamRosterMember::CATEGORY_LABELS[cat], cat]
[TeamRosterMember.category_label(cat), cat]
end
end
def roster_player_role_options(team, selected = nil)
roles = Sports::PlayerRoles.for_team(team)
options_for_select(
[["— Seleziona ruolo —", ""]] + roles.map { |r| [r, r] },
[[t("roster.role_select_blank"), ""]] + roles.map { |r| [r, r] },
selected
)
end
+11 -8
View File
@@ -4,15 +4,18 @@ module Billing
@invoice = params[:invoice]
@club = @invoice.club
attachments["fattura-#{@invoice.number.parameterize}.pdf"] = {
mime_type: "application/pdf",
content: @invoice.pdf.download
}
I18n.with_locale(I18n.locale) do
prefix = t("mailers.invoice.attachment_prefix")
attachments["#{prefix}-#{@invoice.number.parameterize}.pdf"] = {
mime_type: "application/pdf",
content: @invoice.pdf.download
}
mail(
to: @club.billing_email,
subject: "Fattura #{@invoice.display_number} — Match Live TV"
)
mail(
to: @club.billing_email,
subject: t("mailers.invoice.subject", number: @invoice.display_number)
)
end
end
end
end
@@ -9,11 +9,14 @@ module Recordings
@recipient = recipient
@replay_url = recording.replay_url
@expires_at = recording.expires_at
@archive_url = "#{MatchLiveTv.app_public_url.chomp('/')}/clubs/#{@club.id}/replays"
mail(
to: recipient.email,
subject: "Replay pronto — #{recording.title_or_default}"
)
I18n.with_locale(I18n.locale) do
mail(
to: recipient.email,
subject: t("mailers.replay_ready.subject", title: recording.title_or_default)
)
end
end
def replay_expiring_soon(recording:, recipient:)
@@ -24,11 +27,14 @@ module Recordings
@replay_url = recording.replay_url
@expires_at = recording.expires_at
@days_left = ((recording.expires_at - Time.current) / 1.day).ceil
@archive_url = "#{MatchLiveTv.app_public_url.chomp('/')}/clubs/#{@club.id}/replays"
mail(
to: recipient.email,
subject: "Replay in scadenza tra #{@days_left} giorni — #{recording.title_or_default}"
)
I18n.with_locale(I18n.locale) do
mail(
to: recipient.email,
subject: t("mailers.replay_expiring.subject", days: @days_left, title: recording.title_or_default)
)
end
end
end
end
+3 -1
View File
@@ -6,6 +6,8 @@ class UserMailer < ApplicationMailer
@reset_url = public_password_reset_url(token: token)
@expiry_hours = MatchLiveTv.password_reset_expiry_hours
mail to: user.email, subject: "Reimposta la password — Match Live TV"
I18n.with_locale(I18n.locale) do
mail to: user.email, subject: t("mailers.password_reset.subject")
end
end
end
@@ -1,14 +1,16 @@
module ClubBillingProfile
extend ActiveSupport::Concern
BILLING_ENTITY_TYPES = {
"company" => "Società / ASD con P.IVA",
"individual" => "Persona fisica",
"nonprofit" => "Associazione senza scopo di lucro"
}.freeze
BILLING_ENTITY_TYPE_KEYS = %w[company individual nonprofit].freeze
class_methods do
def billing_entity_types
ClubBillingProfile::BILLING_ENTITY_TYPE_KEYS.index_with { |key| I18n.t("billing.entity_types.#{key}") }
end
end
included do
validates :billing_entity_type, inclusion: { in: BILLING_ENTITY_TYPES.keys }, allow_nil: true
validates :billing_entity_type, inclusion: { in: BILLING_ENTITY_TYPE_KEYS }, allow_nil: true
validates :billing_email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true
validates :billing_recipient_code, length: { is: 7 }, allow_blank: true
validates :billing_province, length: { is: 2 }, allow_blank: true
@@ -23,28 +25,28 @@ module ClubBillingProfile
# Campi minimi per intestazione fattura e invio (email PDF + SDI/PEC).
def billing_profile_errors
errors = []
errors << "Tipo intestatario" if billing_entity_type.blank?
errors << "Ragione sociale o nome intestatario" if billing_legal_name.blank?
errors << "Email di fatturazione" if billing_email.blank?
errors << "Indirizzo" if billing_address_line.blank?
errors << "Città" if billing_city.blank?
errors << "Provincia (sigla 2 lettere)" if billing_province.blank? || billing_province.to_s.length != 2
errors << "CAP" if billing_postal_code.blank?
errors << "Paese (ISO)" if billing_country.blank? || billing_country.to_s.length != 2
errors << I18n.t("billing.profile_errors.entity_type") if billing_entity_type.blank?
errors << I18n.t("billing.profile_errors.legal_name") if billing_legal_name.blank?
errors << I18n.t("billing.profile_errors.email") if billing_email.blank?
errors << I18n.t("billing.profile_errors.address") if billing_address_line.blank?
errors << I18n.t("billing.profile_errors.city") if billing_city.blank?
errors << I18n.t("billing.profile_errors.province") if billing_province.blank? || billing_province.to_s.length != 2
errors << I18n.t("billing.profile_errors.postal_code") if billing_postal_code.blank?
errors << I18n.t("billing.profile_errors.country") if billing_country.blank? || billing_country.to_s.length != 2
errors.concat(billing_tax_id_errors)
errors << "Codice destinatario SDI (7 caratteri) o PEC" if billing_recipient_code.blank? && billing_pec.blank?
errors << "Codice destinatario SDI (7 caratteri)" if billing_recipient_code.present? && billing_recipient_code.length != 7
errors << I18n.t("billing.profile_errors.sdi_or_pec") if billing_recipient_code.blank? && billing_pec.blank?
errors << I18n.t("billing.profile_errors.sdi_invalid") if billing_recipient_code.present? && billing_recipient_code.length != 7
errors
end
def billing_tax_id_errors
case billing_entity_type
when "company"
billing_vat_number.blank? ? ["Partita IVA"] : []
billing_vat_number.blank? ? [I18n.t("billing.profile_errors.vat_number")] : []
when "individual"
billing_fiscal_code.blank? ? ["Codice Fiscale"] : []
billing_fiscal_code.blank? ? [I18n.t("billing.profile_errors.fiscal_code")] : []
else
billing_vat_number.blank? && billing_fiscal_code.blank? ? ["P.IVA o Codice Fiscale"] : []
billing_vat_number.blank? && billing_fiscal_code.blank? ? [I18n.t("billing.profile_errors.vat_or_fiscal")] : []
end
end
@@ -56,7 +58,7 @@ module ClubBillingProfile
# Righe per intestazione fattura in admin (etichetta, valore).
def billing_profile_invoice_lines
lines = []
lines << ["Tipo", BILLING_ENTITY_TYPES[billing_entity_type]] if billing_entity_type.present?
lines << ["Tipo", self.class.billing_entity_types[billing_entity_type]] if billing_entity_type.present?
lines << ["Intestatario", billing_legal_name]
lines << ["P.IVA", billing_vat_number] if billing_vat_number.present?
lines << ["Codice fiscale", billing_fiscal_code] if billing_fiscal_code.present?
+4 -4
View File
@@ -47,11 +47,11 @@ class Match < ApplicationRecord
def public_status_label
active = active_stream_session
return "In corso / da riprendere" if active
return "Programmata" if scheduled_upcoming?
return "Pronta" if scheduled_at.present?
return I18n.t("matches.status.active") if active
return I18n.t("matches.status.scheduled") if scheduled_upcoming?
return I18n.t("matches.status.ready") if scheduled_at.present?
"Senza orario"
I18n.t("matches.status.no_time")
end
scope :search_teams_or_opponents, lambda { |query|
+6 -6
View File
@@ -164,13 +164,13 @@ class Recording < ApplicationRecord
end
def status_label
return "Eliminato" if deleted?
return "Scaduto" if status == "expired"
return "Errore" if status == "failed"
return "In elaborazione" if status == "processing"
return "Scade presto" if expires_at.present? && expires_at <= 7.days.from_now
return I18n.t("recordings.status.deleted") if deleted?
return I18n.t("recordings.status.expired") if status == "expired"
return I18n.t("recordings.status.failed") if status == "failed"
return I18n.t("recordings.status.processing") if status == "processing"
return I18n.t("recordings.status.expiring_soon") if expires_at.present? && expires_at <= 7.days.from_now
"Disponibile"
I18n.t("recordings.status.available")
end
private
+12 -13
View File
@@ -2,12 +2,10 @@ class TeamRosterMember < ApplicationRecord
CATEGORIES = %w[staff coach manager player].freeze
# Ordine di visualizzazione in pagina organico
DISPLAY_ORDER = %w[coach manager player staff].freeze
CATEGORY_LABELS = {
"staff" => "Staff",
"coach" => "Allenatori",
"manager" => "Dirigenti",
"player" => "Giocatori"
}.freeze
def self.category_label(category)
I18n.t("roster.category.#{category}", default: category.to_s.capitalize)
end
VOLLEYBALL_PLAYER_ROLES = Sports::PlayerRoles::BY_BOARD["volley"].freeze
@@ -27,7 +25,7 @@ class TeamRosterMember < ApplicationRecord
scope :by_category, ->(cat) { where(category: cat).ordered }
def category_label
CATEGORY_LABELS[category] || category
self.class.category_label(category)
end
def initials
@@ -58,10 +56,11 @@ class TeamRosterMember < ApplicationRecord
def default_role_label
case category
when "coach" then "Allenatore"
when "manager" then "Dirigente"
when "staff" then "Staff"
when "player" then jersey_number.present? ? "Giocatore ##{jersey_number}" : "Giocatore"
when "coach" then I18n.t("roster.role.coach")
when "manager" then I18n.t("roster.role.manager")
when "staff" then I18n.t("roster.role.staff")
when "player"
jersey_number.present? ? I18n.t("roster.role.player_numbered", number: jersey_number) : I18n.t("roster.role.player")
else category
end
end
@@ -70,12 +69,12 @@ class TeamRosterMember < ApplicationRecord
allowed = Sports::PlayerRoles.for_team(team)
return if role_label.in?(allowed)
errors.add(:role_label, "non è un ruolo valido per questo sport")
errors.add(:role_label, I18n.t("roster.errors.role_invalid"))
end
def photo_file_type
return if photo_file.content_type.in?(%w[image/png image/jpeg image/webp])
errors.add(:photo_file, "deve essere PNG, JPEG o WebP")
errors.add(:photo_file, I18n.t("roster.errors.photo_invalid_type"))
end
end
@@ -10,38 +10,37 @@ module Billing
if result.upgrade?
amount = format_amount(result.amount_cents, result.currency)
if amount.present?
"Upgrade a #{plan.name} (#{interval_label}) completato. Addebito immediato: #{amount}."
I18n.t("billing.messages.upgrade_completed_with_amount", plan: plan.name, interval: interval_label, amount: amount)
else
"Upgrade a #{plan.name} (#{interval_label}) completato. L'addebito della differenza è in elaborazione sulla carta salvata."
I18n.t("billing.messages.upgrade_completed_pending_charge", plan: plan.name, interval: interval_label)
end
else
when_label = result.effective_at.present? ? I18n.l(result.effective_at, format: :long) : "il prossimo rinnovo"
current_name = current_plan&.name || "il piano attuale"
"Passaggio a #{plan.name} (#{interval_label}) programmato: resti su #{current_name} fino al #{when_label}. " \
"Nessun addebito aggiuntivo ora; dal prossimo ciclo pagherai il nuovo prezzo."
when_label = result.effective_at.present? ? I18n.l(result.effective_at, format: :long) : I18n.t("billing.messages.next_renewal")
current_name = current_plan&.name || I18n.t("billing.messages.current_plan_generic")
I18n.t("billing.messages.downgrade_scheduled", plan: plan.name, interval: interval_label, current_name: current_name, when: when_label)
end
end
def button_label(plan:, interval:, kind:)
price = PriceCatalog.label(plan_slug: plan.slug, interval: interval)
if kind == :checkout
"Attiva #{plan.name}#{price}"
I18n.t("billing.messages.activate_button", plan: plan.name, price: price)
else
"Passa a #{plan.name}#{price}"
I18n.t("billing.messages.switch_button", plan: plan.name, price: price)
end
end
def billing_info_lines(subscription: nil)
lines = [
"Upgrade (piano superiore o passaggio alla fatturazione annuale): la differenza viene addebitata subito sulla carta salvata in Stripe.",
"Downgrade tra piani a pagamento (es. Full → Light): il nuovo piano è attivo dal prossimo rinnovo; fino ad allora restano le funzioni del piano attuale, senza addebito aggiuntivo ora."
I18n.t("billing.messages.upgrade_info_line"),
I18n.t("billing.messages.downgrade_info_line")
]
if subscription&.plan_change_pending? && subscription.pending_plan.present?
when_at = subscription.current_period_end
when_label = when_at.present? ? I18n.l(when_at, format: :long) : "il prossimo rinnovo"
when_label = when_at.present? ? I18n.l(when_at, format: :long) : I18n.t("billing.messages.next_renewal")
lines.unshift(
"Hai già richiesto #{subscription.pending_plan.name} dal #{when_label}: fino ad allora resta attivo #{subscription.plan.name}."
I18n.t("billing.messages.already_requested_line", plan: subscription.pending_plan.name, when: when_label, current_plan: subscription.plan.name)
)
end
+5 -5
View File
@@ -1,26 +1,26 @@
<div style="max-width:360px;margin:4rem auto">
<h2>Accesso admin</h2>
<h2><%= t("admin.auth.new.title") %></h2>
<% if flash[:alert] %>
<p style="color:#ff6b6b"><%= flash[:alert] %></p>
<% end %>
<%= form_with url: admin_login_path, method: :post, local: true do %>
<p>
<label for="username">Username</label><br>
<label for="username"><%= t("admin.auth.new.username_label") %></label><br>
<input type="text" name="username" id="username" required autofocus autocomplete="username"
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p>
<p>
<label for="password">Password</label><br>
<label for="password"><%= t("admin.auth.new.password_label") %></label><br>
<input type="password" name="password" id="password" required autocomplete="current-password"
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p>
<p>
<button type="submit" style="background:#FF2D2D;color:#fff;border:0;padding:0.6rem 1.2rem;border-radius:6px;cursor:pointer;font-weight:700">
Accedi
<%= t("admin.auth.new.submit") %>
</button>
</p>
<% end %>
<p style="color:#888;font-size:0.85rem;margin-top:1.5rem">
Credenziali iniziali: <code>admin</code> / <code>admin</code>. Cambia la password dopo il primo accesso.
<%= t("admin.auth.new.initial_credentials_html") %>
</p>
</div>
+17 -17
View File
@@ -1,12 +1,12 @@
<h2>Pagamenti da fatturare</h2>
<h2><%= t("admin.billing.index.title") %></h2>
<p style="color:#666;margin-bottom:16px">
Pagamenti Stripe <strong>pagati</strong> senza PDF fattura. Genera il PDF nei tuoi sistemi, poi caricalo qui: viene associato al pagamento e inviato via email al cliente.
<%= t("admin.billing.index.description") %>
</p>
<% if @clubs.many? %>
<p style="margin-bottom:16px">
Filtra società:
<%= link_to "Tutte", admin_billing_path, class: (@filter_club ? nil : "admin-nav-active") %>
<%= t("admin.billing.index.filter_label") %>
<%= link_to t("admin.billing.index.filter_all"), admin_billing_path, class: (@filter_club ? nil : "admin-nav-active") %>
<% @clubs.each do |club| %>
· <%= link_to club.name, admin_billing_path(club_id: club.id), class: (@filter_club&.id == club.id ? "admin-nav-active" : nil) %>
<% end %>
@@ -33,7 +33,7 @@
· <strong><%= payment.formatted_amount %></strong>
</div>
<% unless club.billing_profile_complete? %>
<span class="billing-pending-card__warn">Dati fatturazione incompleti</span>
<span class="billing-pending-card__warn"><%= t("admin.billing.index.incomplete_profile_warning") %></span>
<% end %>
</header>
@@ -46,16 +46,16 @@
<% end %>
</dl>
<% else %>
<p class="billing-pending-card__warn">Nessun dato di fatturazione — il cliente deve completare il profilo.</p>
<p class="billing-pending-card__warn"><%= t("admin.billing.index.no_billing_data") %></p>
<% end %>
<%= form_with url: admin_billing_payment_attach_pdf_path(payment),
method: :post, multipart: true, local: true, class: "billing-upload-form" do %>
<label class="billing-upload-form__label">
PDF fattura
<%= t("admin.billing.index.pdf_label") %>
<%= file_field_tag :pdf, accept: "application/pdf", required: true %>
</label>
<%= submit_tag "Carica PDF e invia al cliente", class: "admin-btn admin-btn--primary" %>
<%= submit_tag t("admin.billing.index.upload_button"), class: "admin-btn admin-btn--primary" %>
<% end %>
</div>
</article>
@@ -63,21 +63,21 @@
</div>
<% else %>
<p class="admin-flash" style="background:#1b3d1b;border-color:#2e7d32">
Nessun pagamento in attesa di fattura<%= @filter_club ? " per #{@filter_club.name}" : "" %>.
<%= t("admin.billing.index.none_pending", club: (@filter_club ? t("admin.billing.index.none_pending_for_club", club: @filter_club.name) : "")) %>
</p>
<% end %>
<% if @completed_payments.any? %>
<h2 style="margin-top:40px;font-size:1.15rem">Fatture già caricate</h2>
<h2 style="margin-top:40px;font-size:1.15rem"><%= t("admin.billing.index.uploaded_title") %></h2>
<table class="admin-table">
<thead>
<tr>
<th>Data</th>
<th>Società</th>
<th>Descrizione</th>
<th>Importo</th>
<th>Fattura</th>
<th>Stato</th>
<th><%= t("admin.billing.index.table.date") %></th>
<th><%= t("admin.billing.index.table.club") %></th>
<th><%= t("admin.billing.index.table.description") %></th>
<th><%= t("admin.billing.index.table.amount") %></th>
<th><%= t("admin.billing.index.table.invoice") %></th>
<th><%= t("admin.billing.index.table.status") %></th>
</tr>
</thead>
<tbody>
@@ -96,4 +96,4 @@
</table>
<% end %>
<p style="margin-top:24px"><%= link_to "← Dashboard", admin_root_path %></p>
<p style="margin-top:24px"><%= link_to t("admin.billing.index.back_dashboard"), admin_root_path %></p>
@@ -1,45 +1,45 @@
<h1>Fattura <%= @invoice.number %><%= @club.name %></h1>
<h1><%= t("admin.billing_invoices.edit.title", number: @invoice.number, club: @club.name) %></h1>
<p style="color:#666">
Stato: <strong><%= @invoice.status %></strong>
<%= t("admin.billing_invoices.edit.status_label") %> <strong><%= @invoice.status %></strong>
<% if @invoice.emailed_at.present? %>
· Inviata il <%= @invoice.emailed_at.in_time_zone.strftime("%d/%m/%Y %H:%M") %>
· <%= t("admin.billing_invoices.edit.sent_at", date: @invoice.emailed_at.in_time_zone.strftime("%d/%m/%Y %H:%M")) %>
<% end %>
</p>
<div class="card">
<%= form_with model: @invoice, url: admin_club_billing_invoice_path(@club, @invoice), multipart: true, method: :patch do |f| %>
<%= f.label :number, "Numero fattura" %>
<%= f.label :number, t("admin.billing_invoices.edit.number_label") %>
<%= f.text_field :number, required: true, disabled: @invoice.status.in?(%w[sent]) %>
<%= f.label :issued_on, "Data emissione" %>
<%= f.label :issued_on, t("admin.billing_invoices.edit.issued_on_label") %>
<%= f.date_field :issued_on, required: true, disabled: @invoice.status.in?(%w[sent]) %>
<%= label_tag :amount_euros, "Importo (€)" %>
<%= label_tag :amount_euros, t("admin.billing_invoices.edit.amount_label") %>
<%= number_field_tag "billing_invoice[amount_euros]",
@invoice.amount_cents / 100.0,
step: 0.01, min: 0.01, required: true, disabled: @invoice.status.in?(%w[sent]) %>
<% if @payment %>
<p style="color:#888;font-size:0.9rem">Pagamento: <%= @payment.display_description %><%= @payment.formatted_amount %></p>
<p style="color:#888;font-size:0.9rem"><%= t("admin.billing_invoices.edit.payment_info", description: @payment.display_description, amount: @payment.formatted_amount) %></p>
<% end %>
<%= f.label :pdf, "PDF fattura" %>
<%= f.label :pdf, t("admin.billing_invoices.edit.pdf_label") %>
<% if @invoice.pdf.attached? %>
<p style="color:#888">PDF già caricato: <%= @invoice.pdf.filename %></p>
<p style="color:#888"><%= t("admin.billing_invoices.edit.pdf_already", filename: @invoice.pdf.filename) %></p>
<% end %>
<%= f.file_field :pdf, accept: "application/pdf" %>
<%= f.label :notes, "Note interne" %>
<%= f.label :notes, t("admin.billing_invoices.edit.notes_label") %>
<%= f.text_area :notes, rows: 2 %>
<div style="margin-top:16px;display:flex;gap:10px;flex-wrap:wrap">
<% unless @invoice.status.in?(%w[sent]) %>
<%= f.submit "Salva bozza", class: "btn btn-secondary" %>
<%= f.submit "Emetti e invia via email", class: "btn btn-primary",
data: { turbo_confirm: "Inviare la fattura a #{@club.billing_email}?" } %>
<%= f.submit t("admin.billing_invoices.edit.save_draft"), name: "commit_action", value: "draft", class: "btn btn-secondary" %>
<%= f.submit t("admin.billing_invoices.edit.issue_send"), name: "commit_action", value: "issue", class: "btn btn-primary",
data: { turbo_confirm: t("admin.billing_invoices.edit.issue_confirm", email: @club.billing_email) } %>
<% end %>
</div>
<% end %>
</div>
<p><%= link_to "← Pagamenti e fatture", admin_club_billing_invoices_path(@club) %></p>
<p><%= link_to t("admin.billing_invoices.edit.back"), admin_club_billing_invoices_path(@club) %></p>
@@ -1,18 +1,18 @@
<h1>Fatturazione — <%= @club.name %></h1>
<h1><%= t("admin.billing_invoices.index.title", club: @club.name) %></h1>
<p>
<%= link_to "← Dashboard", admin_root_path %>
· <%= link_to "Nuova fattura (senza pagamento)", new_admin_club_billing_invoice_path(@club), class: "btn btn-secondary" %>
<%= link_to t("admin.billing_invoices.index.back_dashboard"), admin_root_path %>
· <%= link_to t("admin.billing_invoices.index.new_invoice"), new_admin_club_billing_invoice_path(@club), class: "btn btn-secondary" %>
</p>
<h2 style="margin-top:24px;font-size:1.1rem">Pagamenti</h2>
<h2 style="margin-top:24px;font-size:1.1rem"><%= t("admin.billing_invoices.index.payments_title") %></h2>
<% if @payments.any? %>
<table class="data">
<thead>
<tr>
<th>Data</th>
<th>Descrizione</th>
<th>Importo</th>
<th>Fattura</th>
<th><%= t("admin.billing_invoices.index.table.date") %></th>
<th><%= t("admin.billing_invoices.index.table.description") %></th>
<th><%= t("admin.billing_invoices.index.table.amount") %></th>
<th><%= t("admin.billing_invoices.index.table.invoice") %></th>
<th></th>
</tr>
</thead>
@@ -26,16 +26,16 @@
<td>
<% if inv %>
<%= inv.number %><%= inv.status %>
<% if inv.pdf.attached? %> (PDF)<% end %>
<% if inv.pdf.attached? %><%= t("admin.billing_invoices.index.pdf_suffix") %><% end %>
<% else %>
<%= t("admin.common.dash") %>
<% end %>
</td>
<td>
<% if inv %>
<%= link_to "Modifica / invia PDF", edit_admin_club_billing_invoice_path(@club, inv) %>
<%= link_to t("admin.billing_invoices.index.edit_send_pdf"), edit_admin_club_billing_invoice_path(@club, inv) %>
<% else %>
<%= link_to "Crea fattura", new_admin_club_billing_invoice_path(@club, billing_payment_id: payment.id), class: "btn btn-primary", style: "padding:6px 10px;font-size:0.85rem" %>
<%= link_to t("admin.billing_invoices.index.create_invoice"), new_admin_club_billing_invoice_path(@club, billing_payment_id: payment.id), class: "btn btn-primary", style: "padding:6px 10px;font-size:0.85rem" %>
<% end %>
</td>
</tr>
@@ -43,14 +43,21 @@
</tbody>
</table>
<% else %>
<p>Nessun pagamento registrato per questa società.</p>
<p><%= t("admin.billing_invoices.index.no_payments") %></p>
<% end %>
<h2 style="margin-top:32px;font-size:1.1rem">Tutte le fatture</h2>
<h2 style="margin-top:32px;font-size:1.1rem"><%= t("admin.billing_invoices.index.all_invoices_title") %></h2>
<% if @invoices.any? %>
<table class="data">
<thead>
<tr><th>Numero</th><th>Data</th><th>Importo</th><th>Stato</th><th>Pagamento</th><th></th></tr>
<tr>
<th><%= t("admin.billing_invoices.index.table2.number") %></th>
<th><%= t("admin.billing_invoices.index.table2.date") %></th>
<th><%= t("admin.billing_invoices.index.table2.amount") %></th>
<th><%= t("admin.billing_invoices.index.table2.status") %></th>
<th><%= t("admin.billing_invoices.index.table2.payment") %></th>
<th></th>
</tr>
</thead>
<tbody>
<% @invoices.each do |inv| %>
@@ -59,12 +66,12 @@
<td><%= inv.issued_on %></td>
<td><%= inv.formatted_amount %></td>
<td><%= inv.status %></td>
<td><%= inv.billing_payment_id.present? ? "Sì" : "No" %></td>
<td><%= link_to "Modifica", edit_admin_club_billing_invoice_path(@club, inv) %></td>
<td><%= inv.billing_payment_id.present? ? t("admin.common.yes") : t("admin.common.no") %></td>
<td><%= link_to t("admin.billing_invoices.index.edit"), edit_admin_club_billing_invoice_path(@club, inv) %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>Nessuna fattura.</p>
<p><%= t("admin.billing_invoices.index.no_invoices") %></p>
<% end %>
@@ -1,32 +1,32 @@
<h1>Nuova fattura — <%= @club.name %></h1>
<h1><%= t("admin.billing_invoices.new.title", club: @club.name) %></h1>
<% if @payment %>
<p style="color:#666">Collegata al pagamento del <%= @payment.paid_at&.to_date || @payment.created_at.to_date %> (<%= @payment.formatted_amount %>).</p>
<p style="color:#666"><%= t("admin.billing_invoices.new.linked_payment", date: (@payment.paid_at&.to_date || @payment.created_at.to_date), amount: @payment.formatted_amount) %></p>
<% end %>
<div class="card">
<%= form_with model: @invoice, url: admin_club_billing_invoices_path(@club) do |f| %>
<%= f.hidden_field :billing_payment_id if @payment %>
<%= f.label :number, "Numero fattura" %>
<%= f.label :number, t("admin.billing_invoices.new.number_label") %>
<%= f.text_field :number, required: true %>
<%= f.label :issued_on, "Data emissione" %>
<%= f.label :issued_on, t("admin.billing_invoices.new.issued_on_label") %>
<%= f.date_field :issued_on, required: true %>
<%= label_tag :amount_euros, "Importo (€)" %>
<%= label_tag :amount_euros, t("admin.billing_invoices.new.amount_label") %>
<%= number_field_tag "billing_invoice[amount_euros]",
(@invoice.amount_cents ? @invoice.amount_cents / 100.0 : nil),
step: 0.01, min: 0.01, required: true %>
<%= f.label :notes, "Note interne (opzionale)" %>
<%= f.label :notes, t("admin.billing_invoices.new.notes_label") %>
<%= f.text_area :notes, rows: 2 %>
<p style="color:#666;font-size:0.9rem;margin-top:12px">
Salva la bozza, poi carica il PDF e inviala al cliente dalla schermata successiva.
<%= t("admin.billing_invoices.new.hint") %>
</p>
<%= f.submit "Crea bozza fattura", class: "btn btn-primary" %>
<%= f.submit t("admin.billing_invoices.new.submit"), class: "btn btn-primary" %>
<% end %>
</div>
<p><%= link_to "← Pagamenti e fatture", admin_club_billing_invoices_path(@club) %></p>
<p><%= link_to t("admin.billing_invoices.new.back"), admin_club_billing_invoices_path(@club) %></p>
@@ -1,4 +1,4 @@
<% content_for :title, "Replay — #{@club.name}" %>
<% content_for :title, t("admin.club_recordings.index.title", club: @club.name) %>
<% content_for :replay_archive_styles, true %>
<%= render "recordings/club_archive", admin_mode: true %>
@@ -1,43 +1,43 @@
<%# locals: (club:, subscription:, return_to: nil) %>
<section class="admin-comped-card" style="margin-bottom:24px;padding:16px;border:1px solid #3d3520;border-radius:8px;background:#2a2618">
<h2 style="margin:0 0 8px;font-size:1.1rem">Abbonamento omaggio</h2>
<h2 style="margin:0 0 8px;font-size:1.1rem"><%= t("admin.comped.title") %></h2>
<p style="color:#bbb;font-size:0.9rem;margin:0 0 14px">
Sponsor o promozione: assegna Premium Light/Full senza pagamento Stripe. Revocabile in qualsiasi momento.
<%= t("admin.comped.description") %>
</p>
<% sub = subscription %>
<% if sub&.admin_comped? %>
<p style="margin:0 0 12px">
<strong>Attivo:</strong> <%= sub.plan.name %>
<strong><%= t("admin.comped.active_label") %></strong> <%= sub.plan.name %>
<% if sub.admin_comped_reason.present? %>
· <%= sub.admin_comped_reason %>
<% end %>
<% if sub.admin_comped_at.present? %>
<br><span style="color:#888;font-size:0.85rem">Dal <%= l(sub.admin_comped_at, format: :long) %>
<% if sub.admin_comped_by.present? %> · admin <%= sub.admin_comped_by.username %><% end %>
<br><span style="color:#888;font-size:0.85rem"><%= t("admin.comped.since_label_html", date: l(sub.admin_comped_at, format: :long)) %>
<% if sub.admin_comped_by.present? %> <%= t("admin.comped.by_admin", username: sub.admin_comped_by.username) %><% end %>
</span>
<% end %>
<% if sub.stripe_subscription_id.present? %>
<br><span style="color:#888;font-size:0.85rem">Nota: esiste anche un abbonamento Stripe collegato; lomaggio ha priorità sul piano.</span>
<br><span style="color:#888;font-size:0.85rem"><%= t("admin.comped.stripe_note") %></span>
<% end %>
</p>
<%= button_to "Revoca omaggio (torna Free o ripristina Stripe)",
<%= button_to t("admin.comped.revoke_button"),
revoke_comped_admin_club_path(club, return_to: return_to),
method: :delete,
class: "admin-btn admin-btn--secondary",
form: { data: { turbo_confirm: "Revocare labbonamento omaggio per #{club.name}?" } } %>
form: { data: { turbo_confirm: t("admin.comped.revoke_confirm", club: club.name) } } %>
<% else %>
<p style="margin:0 0 12px;color:#888">
Piano attuale: <strong><%= sub&.plan&.name || "Free" %></strong>
<%= t("admin.comped.current_plan", plan: sub&.plan&.name || t("admin.common.free_plan")) %>
<% if sub&.stripe_subscription_id.present? %>
· pagamento Stripe attivo
<%= t("admin.comped.stripe_active") %>
<% end %>
</p>
<%= form_with url: grant_comped_admin_club_path(club), method: :post, local: true, class: "admin-comped-form" do %>
<%= hidden_field_tag :return_to, return_to if return_to.present? %>
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end">
<label style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem">
Piano
<%= t("admin.comped.plan_label") %>
<%= select_tag :plan_slug,
options_for_select(
[["Premium Light", "premium_light"], ["Premium Full", "premium_full"]]
@@ -46,10 +46,10 @@
class: "admin-input" %>
</label>
<label style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;flex:1;min-width:200px">
Motivo (es. sponsor 2026)
<%= text_field_tag :reason, nil, placeholder: "Sponsor, promozione…", class: "admin-input", style: "width:100%" %>
<%= t("admin.comped.reason_label") %>
<%= text_field_tag :reason, nil, placeholder: t("admin.comped.reason_placeholder"), class: "admin-input", style: "width:100%" %>
</label>
<%= submit_tag "Concedi omaggio", class: "admin-btn admin-btn--primary" %>
<%= submit_tag t("admin.comped.grant_button"), class: "admin-btn admin-btn--primary" %>
</div>
<% end %>
<% end %>
+13 -14
View File
@@ -1,17 +1,16 @@
<h2>Società e squadre</h2>
<h2><%= t("admin.clubs.index.title") %></h2>
<p style="color:#888;margin-bottom:16px">
Abbonamenti omaggio (sponsor / promozioni) e stato YouTube per squadra.
Pagamenti Stripe: <%= link_to "Fatturazione", admin_billing_path %>.
<%= t("admin.clubs.index.subtitle_html", billing_link: link_to(t("admin.clubs.index.billing_link"), admin_billing_path)) %>
</p>
<table class="admin-table">
<thead>
<tr>
<th>Società</th>
<th>Piano</th>
<th>Squadre</th>
<th>Omaggio</th>
<th>Stripe</th>
<th><%= t("admin.clubs.index.table.club") %></th>
<th><%= t("admin.clubs.index.table.plan") %></th>
<th><%= t("admin.clubs.index.table.teams") %></th>
<th><%= t("admin.clubs.index.table.comped") %></th>
<th><%= t("admin.clubs.index.table.stripe") %></th>
<th></th>
</tr>
</thead>
@@ -20,20 +19,20 @@
<% sub = club.subscription %>
<tr>
<td><strong><%= club.name %></strong></td>
<td><%= sub&.plan&.name || "Free" %></td>
<td><%= sub&.plan&.name || t("admin.common.free_plan") %></td>
<td><%= club.teams.size %></td>
<td>
<% if sub&.admin_comped? %>
<span style="color:#ffb74d"></span>
<span style="color:#ffb74d"><%= t("admin.common.yes") %></span>
<% if sub.admin_comped_reason.present? %> · <%= sub.admin_comped_reason %><% end %>
<% else %>
<%= t("admin.common.dash") %>
<% end %>
</td>
<td><%= sub&.stripe_subscription_id.present? ? "Sì" : "—" %></td>
<td><%= sub&.stripe_subscription_id.present? ? t("admin.common.yes") : t("admin.common.dash") %></td>
<td>
<%= link_to "Gestisci", admin_club_path(club) %>
· <%= link_to "Fatture", admin_billing_path(club_id: club.id) %>
<%= link_to t("admin.clubs.index.manage"), admin_club_path(club) %>
· <%= link_to t("admin.clubs.index.invoices"), admin_billing_path(club_id: club.id) %>
</td>
</tr>
<% end %>
+14 -14
View File
@@ -1,36 +1,36 @@
<p style="margin-bottom:16px"><%= link_to "← Società e squadre", admin_clubs_path %></p>
<p style="margin-bottom:16px"><%= link_to t("admin.clubs.show.back"), admin_clubs_path %></p>
<h2><%= @club.name %></h2>
<p style="color:#888">
Sport: <%= @club.sport %>
· <%= link_to "Archivio replay", admin_club_recordings_path(@club) %>
· <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @club.id) %>
· <%= link_to "Canale YouTube piattaforma", admin_youtube_platform_path %>
<%= t("admin.clubs.show.sport_label", sport: @club.sport) %>
· <%= link_to t("admin.clubs.show.recordings_archive"), admin_club_recordings_path(@club) %>
· <%= link_to t("admin.clubs.show.billing_link"), admin_billing_path(club_id: @club.id) %>
· <%= link_to t("admin.clubs.show.youtube_platform_link"), admin_youtube_platform_path %>
</p>
<%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %>
<% cred = @club.youtube_credential %>
<p style="margin-top:16px">
<strong>YouTube società:</strong>
<strong><%= t("admin.clubs.show.youtube_club_label") %></strong>
<% if cred %>
<%= cred.channel_title.presence || cred.channel_id %> (collegato)
<%= cred.channel_title.presence || cred.channel_id %> <%= t("admin.clubs.show.youtube_connected") %>
<% elsif @teams.any? { |t| t.entitlements.premium_full? } %>
<span class="muted">Premium Full — canale società non collegato (usa Match Live TV)</span>
<span class="muted"><%= t("admin.clubs.show.youtube_premium_full_not_connected") %></span>
<% else %>
<span class="muted"></span>
<span class="muted"><%= t("admin.common.dash") %></span>
<% end %>
</p>
<h3 style="font-size:1rem;margin-top:28px">Squadre</h3>
<h3 style="font-size:1rem;margin-top:28px"><%= t("admin.clubs.show.teams_title") %></h3>
<% if @teams.empty? %>
<p class="muted">Nessuna squadra registrata.</p>
<p class="muted"><%= t("admin.clubs.show.no_teams") %></p>
<% else %>
<table class="admin-table">
<thead>
<tr>
<th>Squadra</th>
<th>Sport</th>
<th><%= t("admin.clubs.show.table.team") %></th>
<th><%= t("admin.clubs.show.table.sport") %></th>
<th></th>
</tr>
</thead>
@@ -39,7 +39,7 @@
<tr>
<td><strong><%= team.name %></strong></td>
<td><%= team.sport %></td>
<td><%= link_to "Partite e dettagli", admin_team_path(team) %> · <%= link_to "Replay", admin_club_recordings_path(@club, team_id: team.id) %></td>
<td><%= link_to t("admin.clubs.show.matches_and_details"), admin_team_path(team) %> · <%= link_to t("admin.clubs.show.replay"), admin_club_recordings_path(@club, team_id: team.id) %></td>
</tr>
<% end %>
</tbody>
@@ -2,147 +2,153 @@
<section class="kpi-grid">
<div class="kpi-card kpi-card--accent">
<div class="kpi-label">Sessioni attive</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.active_sessions") %></div>
<div class="kpi-value kpi-value--live" id="kpi-active-sessions"><%= @stats[:active_sessions] %></div>
<div class="kpi-sub">in diretta ora</div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.active_sessions_sub") %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Trasmesse oggi</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.today") %></div>
<div class="kpi-value" id="kpi-sessions-today"><%= @stats[:sessions_today] %></div>
<div class="kpi-sub"><%= format_duration_minutes(@stats[:stream_minutes_today]) %> di streaming</div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.today_sub", duration: format_duration_minutes(@stats[:stream_minutes_today])) %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Trasmesse questo mese</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.month") %></div>
<div class="kpi-value" id="kpi-sessions-month"><%= @stats[:sessions_month] %></div>
<div class="kpi-sub"><%= format_duration_minutes(@stats[:stream_minutes_month]) %> totali</div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.month_sub", duration: format_duration_minutes(@stats[:stream_minutes_month])) %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Squadre</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.teams") %></div>
<div class="kpi-value"><%= @stats[:teams_count] %></div>
<div class="kpi-sub"><%= @stats[:users_count] %> utenti</div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.teams_sub", count: @stats[:users_count]) %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">CPU</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.cpu") %></div>
<div class="kpi-value" id="kpi-cpu"><%= @host[:cpu] %>%</div>
<div class="kpi-sub">load <%= @host[:load_avg]["1m"] %> / <%= @host[:load_avg]["5m"] %> / <%= @host[:load_avg]["15m"] %></div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.cpu_sub", one: @host[:load_avg]["1m"], five: @host[:load_avg]["5m"], fifteen: @host[:load_avg]["15m"]) %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">RAM</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.ram") %></div>
<div class="kpi-value" id="kpi-mem"><%= @host[:memory][:used_percent] %>%</div>
<div class="kpi-sub"><%= format_bytes(@host[:memory][:used_bytes]) %> / <%= format_bytes(@host[:memory][:total_bytes]) %></div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.ram_sub", used: format_bytes(@host[:memory][:used_bytes]), total: format_bytes(@host[:memory][:total_bytes])) %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Banda (totale)</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.network") %></div>
<div class="kpi-value" style="font-size:1.2rem" id="kpi-network"><%= format_bytes(@host[:network][:total_bytes]) %></div>
<div class="kpi-sub"><%= format_bytes(@host[:network][:tx_bytes_total]) %> · ↓ <%= format_bytes(@host[:network][:rx_bytes_total]) %></div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.network_sub", tx: format_bytes(@host[:network][:tx_bytes_total]), rx: format_bytes(@host[:network][:rx_bytes_total])) %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Replay in archivio</div>
<div class="kpi-label"><%= t("admin.dashboard.kpi.recordings") %></div>
<div class="kpi-value"><%= @stats[:recordings_ready] %></div>
<div class="kpi-sub"><%= @stats[:recordings_count] %> registrazioni</div>
<div class="kpi-sub"><%= t("admin.dashboard.kpi.recordings_sub", count: @stats[:recordings_count]) %></div>
</div>
</section>
<section class="charts-grid">
<div class="chart-card">
<h3>CPU (%)</h3>
<h3><%= t("admin.dashboard.charts.cpu") %></h3>
<div class="chart-wrap"><canvas id="chart-cpu"></canvas></div>
</div>
<div class="chart-card">
<h3>RAM (%)</h3>
<h3><%= t("admin.dashboard.charts.ram") %></h3>
<div class="chart-wrap"><canvas id="chart-mem"></canvas></div>
</div>
</section>
<section class="panel" style="margin-bottom:1.25rem">
<h2>Stato piattaforma (Ops)</h2>
<h2><%= t("admin.dashboard.ops_panel.title") %></h2>
<% critical = Ops::Incident.critical_open.count %>
<% warnings = Ops::Incident.open.where(severity: "warning").count %>
<% if critical.positive? %>
<p class="admin-flash" style="margin-bottom:0.75rem"><%= critical %> incidente/i critico/i aperto/i — <%= link_to "Vedi dashboard Ops", admin_ops_path %></p>
<p class="admin-flash" style="margin-bottom:0.75rem"><%= t("admin.dashboard.ops_panel.critical", count: critical) %><%= link_to t("admin.dashboard.ops_panel.view_dashboard"), admin_ops_path %></p>
<% elsif warnings.positive? %>
<p class="kpi-sub" style="margin-bottom:0.75rem"><%= warnings %> warning — <%= link_to "Dashboard Ops", admin_ops_path %></p>
<p class="kpi-sub" style="margin-bottom:0.75rem"><%= t("admin.dashboard.ops_panel.warnings", count: warnings) %><%= link_to t("admin.dashboard.ops_panel.dashboard_link"), admin_ops_path %></p>
<% else %>
<p class="empty" style="margin:0">Nessun incidente aperto. <%= link_to "Dashboard Ops", admin_ops_path %></p>
<p class="empty" style="margin:0"><%= t("admin.dashboard.ops_panel.none_html", link: link_to(t("admin.dashboard.ops_panel.dashboard_link"), admin_ops_path)) %></p>
<% end %>
</section>
<section class="charts-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
<div class="chart-card">
<h3>Disco sistema</h3>
<h3><%= t("admin.dashboard.disk.system_title") %></h3>
<% root = @host[:disk][:root] %>
<% if root[:total_bytes] %>
<div class="disk-row">
<header><span><%= root[:path] %></span><span><%= format_bytes(root[:used_bytes]) %> / <%= format_bytes(root[:total_bytes]) %></span></header>
<div class="progress-bar <%= 'progress-bar--ok' if root[:used_percent].to_f < 80 %>"><span style="width:<%= root[:used_percent] %>%"></span></div>
<p class="kpi-sub" style="margin-top:0.35rem">Libero: <%= format_bytes(root[:free_bytes]) %> (<%= root[:used_percent] %>% usato)</p>
<p class="kpi-sub" style="margin-top:0.35rem"><%= t("admin.dashboard.disk.free_label", free: format_bytes(root[:free_bytes]), percent: root[:used_percent]) %></p>
</div>
<% else %>
<p class="empty">Metriche disco non disponibili</p>
<p class="empty"><%= t("admin.dashboard.disk.not_available") %></p>
<% end %>
</div>
<div class="chart-card">
<h3>Archivio registrazioni</h3>
<h3><%= t("admin.dashboard.disk.recordings_title") %></h3>
<% rec = @host[:disk][:recordings] %>
<% if rec[:total_bytes] %>
<div class="disk-row">
<header><span><%= rec[:path] %></span><span><%= format_bytes(rec[:used_bytes]) %> / <%= format_bytes(rec[:total_bytes]) %></span></header>
<div class="progress-bar <%= 'progress-bar--ok' if rec[:used_percent].to_f < 80 %>"><span style="width:<%= rec[:used_percent] %>%"></span></div>
<% if rec[:dir_size_bytes] %>
<p class="kpi-sub" style="margin-top:0.35rem">Contenuto registrazioni: <%= format_bytes(rec[:dir_size_bytes]) %></p>
<p class="kpi-sub" style="margin-top:0.35rem"><%= t("admin.dashboard.disk.recordings_content", size: format_bytes(rec[:dir_size_bytes])) %></p>
<% end %>
</div>
<% else %>
<p class="empty">Percorso <%= Admin::HostMetrics::RECORDINGS_PATH %> non montato</p>
<p class="empty"><%= t("admin.dashboard.disk.recordings_not_mounted", path: Admin::HostMetrics::RECORDINGS_PATH) %></p>
<% end %>
</div>
</section>
<section class="admin-panels">
<div class="panel">
<h2>Sessioni attive</h2>
<h2><%= t("admin.dashboard.sessions.title") %></h2>
<% if @active_sessions.any? %>
<table class="admin-table">
<thead>
<tr><th>Partita</th><th>Stato</th><th>Inizio</th><th>Link</th><th></th></tr>
<tr>
<th><%= t("admin.dashboard.sessions.table.match") %></th>
<th><%= t("admin.dashboard.sessions.table.status") %></th>
<th><%= t("admin.dashboard.sessions.table.start") %></th>
<th><%= t("admin.dashboard.sessions.table.link") %></th>
<th></th>
</tr>
</thead>
<tbody id="active-sessions-body">
<% @active_sessions.each do |s| %>
<tr>
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
<td><span class="badge badge--<%= s.status == 'live' ? 'live' : (s.status == 'paused' ? 'paused' : 'connecting') %>"><%= s.status %></span></td>
<td class="muted"><%= s.started_at&.strftime("%d/%m %H:%M") || "—" %></td>
<td class="muted"><%= s.started_at&.strftime("%d/%m %H:%M") || t("admin.common.dash") %></td>
<td class="admin-link-compact">
<% admin_session_watch_links(s).each do |link| %>
<%= link_to link[:label], link[:url], target: "_blank", rel: "noopener", class: "admin-link-chip" %>
<% end %>
<%= link_to "Regia", admin_session_path(s), class: "admin-link-chip admin-link-chip--regia" %>
<%= link_to t("admin.dashboard.sessions.regia"), admin_session_path(s), class: "admin-link-chip admin-link-chip--regia" %>
</td>
<td class="admin-actions">
<%= link_to "Dettaglio", admin_session_path(s) %>
<%= button_to "Termina", stop_admin_session_path(s), method: :post, class: "admin-btn admin-btn--danger admin-btn--sm", form: { data: { turbo_confirm: "Terminare questa sessione?" } } %>
<%= link_to t("admin.dashboard.sessions.detail"), admin_session_path(s) %>
<%= button_to t("admin.dashboard.sessions.stop"), stop_admin_session_path(s), method: :post, class: "admin-btn admin-btn--danger admin-btn--sm", form: { data: { turbo_confirm: t("admin.dashboard.sessions.stop_confirm") } } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p class="empty">Nessuna sessione attiva in questo momento.</p>
<p class="empty"><%= t("admin.dashboard.sessions.none") %></p>
<% end %>
</div>
<div class="panel">
<h2>Squadre</h2>
<h2><%= t("admin.dashboard.teams.title") %></h2>
<ul class="team-list">
<% @teams.each do |t| %>
<% @teams.each do |t_| %>
<li>
<%= link_to t.name, admin_team_path(t) %>
<span class="muted"><%= t.matches.count %> partite</span>
<%= link_to t_.name, admin_team_path(t_) %>
<span class="muted"><%= t("admin.dashboard.teams.matches_count", count: t_.matches.count) %></span>
</li>
<% end %>
</ul>
<% if @stats[:teams_count] > @teams.size %>
<p class="kpi-sub" style="margin-top:0.75rem"><%= link_to "Vedi società (#{@stats[:teams_count]} squadre)", admin_clubs_path %></p>
<p class="kpi-sub" style="margin-top:0.75rem"><%= link_to t("admin.dashboard.teams.view_all", count: @stats[:teams_count]), admin_clubs_path %></p>
<% end %>
</div>
</section>
+22 -16
View File
@@ -2,30 +2,30 @@
<section class="kpi-grid">
<div class="kpi-card <%= @summary[:open_critical].positive? ? 'kpi-card--danger' : 'kpi-card--accent' %>">
<div class="kpi-label">Critici aperti</div>
<div class="kpi-label"><%= t("admin.ops.kpi.critical") %></div>
<div class="kpi-value"><%= @summary[:open_critical] %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Warning aperti</div>
<div class="kpi-label"><%= t("admin.ops.kpi.warning") %></div>
<div class="kpi-value"><%= @summary[:open_warning] %></div>
</div>
<div class="kpi-card">
<div class="kpi-label">Totale aperti</div>
<div class="kpi-label"><%= t("admin.ops.kpi.total") %></div>
<div class="kpi-value"><%= @summary[:open_total] %></div>
</div>
</section>
<div class="panel" style="margin-bottom:1.5rem">
<h2>Incidenti aperti</h2>
<h2><%= t("admin.ops.open.title") %></h2>
<% if @open_incidents.any? %>
<table class="admin-table">
<thead>
<tr>
<th>Severità</th>
<th>Tipo</th>
<th>Titolo</th>
<th>Occ.</th>
<th>Ultimo</th>
<th><%= t("admin.ops.open.table.severity") %></th>
<th><%= t("admin.ops.open.table.kind") %></th>
<th><%= t("admin.ops.open.table.title") %></th>
<th><%= t("admin.ops.open.table.occurrences") %></th>
<th><%= t("admin.ops.open.table.last_seen") %></th>
<th></th>
</tr>
</thead>
@@ -44,26 +44,32 @@
<td class="muted"><%= inc.last_seen_at&.strftime("%d/%m %H:%M") %></td>
<td class="admin-actions">
<% unless inc.status == 'acknowledged' %>
<%= button_to "Preso in carico", acknowledge_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm" %>
<%= button_to t("admin.ops.actions.acknowledge"), acknowledge_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm" %>
<% end %>
<%= button_to "Risolvi", resolve_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm" %>
<%= button_to "Mute 24h", mute_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm admin-btn--outline" %>
<%= button_to t("admin.ops.actions.resolve"), resolve_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm" %>
<%= button_to t("admin.ops.actions.mute"), mute_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm admin-btn--outline" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p class="empty">Nessun incidente aperto. La piattaforma risulta sana.</p>
<p class="empty"><%= t("admin.ops.open.none") %></p>
<% end %>
</div>
<div class="panel">
<h2>Risolti (ultimi 30 giorni)</h2>
<h2><%= t("admin.ops.resolved.title") %></h2>
<% if @resolved_incidents.any? %>
<table class="admin-table">
<thead>
<tr><th>Severità</th><th>Tipo</th><th>Titolo</th><th>Occ.</th><th>Risolto</th></tr>
<tr>
<th><%= t("admin.ops.open.table.severity") %></th>
<th><%= t("admin.ops.open.table.kind") %></th>
<th><%= t("admin.ops.open.table.title") %></th>
<th><%= t("admin.ops.open.table.occurrences") %></th>
<th><%= t("admin.ops.resolved.table.resolved_at") %></th>
</tr>
</thead>
<tbody>
<% @resolved_incidents.each do |inc| %>
@@ -78,6 +84,6 @@
</tbody>
</table>
<% else %>
<p class="empty">Nessun incidente risolto di recente.</p>
<p class="empty"><%= t("admin.ops.resolved.none") %></p>
<% end %>
</div>
@@ -1,29 +1,29 @@
<div style="max-width:400px">
<h2>Cambia password</h2>
<h2><%= t("admin.passwords.edit.title") %></h2>
<% if flash[:alert] %>
<p style="color:#ff6b6b"><%= flash[:alert] %></p>
<% end %>
<%= form_with url: admin_password_path, method: :patch, local: true do %>
<p>
<label for="current_password">Password attuale</label><br>
<label for="current_password"><%= t("admin.passwords.edit.current_password_label") %></label><br>
<input type="password" name="current_password" id="current_password" required autocomplete="current-password"
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p>
<p>
<label for="password">Nuova password (min. 8 caratteri)</label><br>
<label for="password"><%= t("admin.passwords.edit.new_password_label") %></label><br>
<input type="password" name="password" id="password" required autocomplete="new-password"
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p>
<p>
<label for="password_confirmation">Conferma nuova password</label><br>
<label for="password_confirmation"><%= t("admin.passwords.edit.confirm_password_label") %></label><br>
<input type="password" name="password_confirmation" id="password_confirmation" required autocomplete="new-password"
style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p>
<p>
<button type="submit" style="background:#FF2D2D;color:#fff;border:0;padding:0.6rem 1.2rem;border-radius:6px;cursor:pointer;font-weight:700">
Salva password
<%= t("admin.passwords.edit.submit") %>
</button>
<%= link_to "Annulla", admin_root_path %>
<%= link_to t("admin.passwords.edit.cancel"), admin_root_path %>
</p>
<% end %>
</div>
@@ -1,6 +1,6 @@
<% watch_links = admin_session_watch_links(session) %>
<section class="admin-links-panel">
<h3>Link diretta</h3>
<h3><%= t("admin.sessions.links.watch_title") %></h3>
<% if watch_links.any? %>
<ul class="admin-link-list">
<% watch_links.each do |link| %>
@@ -11,28 +11,28 @@
<% end %>
</ul>
<% else %>
<p class="muted">Nessun link video disponibile per questa sessione.</p>
<p class="muted"><%= t("admin.sessions.links.none") %></p>
<% end %>
<h3>Link regia</h3>
<h3><%= t("admin.sessions.links.regia_title") %></h3>
<% if session.terminal? %>
<p class="muted">Sessione terminata — link regia non disponibile.</p>
<p class="muted"><%= t("admin.sessions.links.terminated") %></p>
<% else %>
<% if flash[:regia_url].present? %>
<p class="admin-link-generated">
<span class="admin-link-label">Regia</span>
<span class="admin-link-label"><%= t("admin.sessions.links.regia_label") %></span>
<%= link_to flash[:regia_url], flash[:regia_url], target: "_blank", rel: "noopener", class: "admin-link-url" %>
</p>
<p><code class="admin-link-code"><%= flash[:regia_url] %></code></p>
<% if (expires = admin_regia_expires_label(flash[:regia_expires_at])) %>
<p class="muted">Valido fino a <%= expires %></p>
<p class="muted"><%= t("admin.sessions.links.valid_until", date: expires) %></p>
<% end %>
<% elsif session.regia_token_active? %>
<p class="muted">Esiste già un link regia attivo (lURL non è recuperabile). Generane uno nuovo se serve condividerlo di nuovo.</p>
<p class="muted"><%= t("admin.sessions.links.active_exists") %></p>
<% else %>
<p class="muted">Nessun link regia attivo. Genera un link da condividere con chi gestisce il punteggio.</p>
<p class="muted"><%= t("admin.sessions.links.none_active") %></p>
<% end %>
<%= button_to "Genera link regia",
<%= button_to t("admin.sessions.links.generate_button"),
regia_link_admin_session_path(session),
method: :post,
class: "admin-btn admin-btn--secondary admin-btn--sm",
@@ -1,6 +1,14 @@
<h2>Stream Sessions</h2>
<h2><%= t("admin.sessions.index.title") %></h2>
<table class="admin-table">
<thead><tr><th>Match</th><th>Status</th><th>Disconnects</th><th>Link</th><th></th></tr></thead>
<thead>
<tr>
<th><%= t("admin.sessions.index.table.match") %></th>
<th><%= t("admin.sessions.index.table.status") %></th>
<th><%= t("admin.sessions.index.table.disconnects") %></th>
<th><%= t("admin.sessions.index.table.link") %></th>
<th></th>
</tr>
</thead>
<tbody>
<% @sessions.each do |s| %>
<tr>
@@ -11,9 +19,9 @@
<% admin_session_watch_links(s).each do |link| %>
<%= link_to link[:label], link[:url], target: "_blank", rel: "noopener", class: "admin-link-chip" %>
<% end %>
<%= link_to "Regia", admin_session_path(s), class: "admin-link-chip admin-link-chip--regia" %>
<%= link_to t("admin.sessions.index.regia"), admin_session_path(s), class: "admin-link-chip admin-link-chip--regia" %>
</td>
<td><%= link_to "Dettaglio", admin_session_path(s) %></td>
<td><%= link_to t("admin.sessions.index.detail"), admin_session_path(s) %></td>
</tr>
<% end %>
</tbody>
+15 -9
View File
@@ -1,26 +1,32 @@
<h2>Session <%= @session.id %></h2>
<p>Status: <strong><%= @session.status %></strong></p>
<h2><%= t("admin.sessions.show.title", id: @session.id) %></h2>
<p><%= t("admin.sessions.show.status_label") %> <strong><%= @session.status %></strong></p>
<% unless @session.terminal? %>
<p>
<%= button_to "Termina sessione",
<%= button_to t("admin.sessions.show.stop_button"),
stop_admin_session_path(@session),
method: :post,
class: "admin-btn admin-btn--danger",
form: { data: { turbo_confirm: "Terminare la trasmissione? Il path RTMP verrà rimosso e lo stato passerà a ended." } } %>
form: { data: { turbo_confirm: t("admin.sessions.show.stop_confirm") } } %>
</p>
<% end %>
<p>Match: <%= @session.match.team.name %> vs <%= @session.match.opponent_name %></p>
<p><%= t("admin.sessions.show.match_label", team: @session.match.team.name, opponent: @session.match.opponent_name) %></p>
<%= render "admin/sessions/links", session: @session %>
<% if @session.youtube_broadcast_id %>
<p>YouTube Studio: <a href="https://studio.youtube.com/video/<%= @session.youtube_broadcast_id %>/livestreaming" target="_blank" rel="noopener">Broadcast</a></p>
<p><%= t("admin.sessions.show.youtube_studio") %>: <a href="https://studio.youtube.com/video/<%= @session.youtube_broadcast_id %>/livestreaming" target="_blank" rel="noopener"><%= t("admin.sessions.show.broadcast") %></a></p>
<% end %>
<p>RTMP ingest: <code><%= @session.rtmp_ingest_url %></code></p>
<p><%= t("admin.sessions.show.rtmp_ingest") %> <code><%= @session.rtmp_ingest_url %></code></p>
<h3>Eventi</h3>
<h3><%= t("admin.sessions.show.events_title") %></h3>
<table>
<thead><tr><th>Tipo</th><th>Quando</th><th>Meta</th></tr></thead>
<thead>
<tr>
<th><%= t("admin.sessions.show.table.type") %></th>
<th><%= t("admin.sessions.show.table.when") %></th>
<th><%= t("admin.sessions.show.table.meta") %></th>
</tr>
</thead>
<tbody>
<% @events.each do |e| %>
<tr>
+13 -11
View File
@@ -1,34 +1,36 @@
<% if @team.club %>
<p style="margin-bottom:16px"><%= link_to "← #{@team.club.name}", admin_club_path(@team.club) %></p>
<p style="margin-bottom:16px"><%= link_to t("admin.teams.show.back", club: @team.club.name), admin_club_path(@team.club) %></p>
<% end %>
<h2><%= @team.name %></h2>
<p>Sport: <%= @team.sport %></p>
<p><%= t("admin.teams.show.sport_label", sport: @team.sport) %></p>
<% if @team.club %>
<p>Società: <%= @team.club.name %> · <%= link_to "Replay squadra", admin_club_recordings_path(@team.club, team_id: @team.id) %> · <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @team.club.id) %></p>
<p><%= t("admin.teams.show.club_label_html",
club: @team.club.name,
replay_link: link_to(t("admin.teams.show.replay_link"), admin_club_recordings_path(@team.club, team_id: @team.id)),
billing_link: link_to(t("admin.teams.show.billing_link"), admin_billing_path(club_id: @team.club.id))) %></p>
<% end %>
<% yt = Youtube::TeamStatus.new(@team) %>
<p>
YouTube:
<%= t("admin.teams.show.youtube_label") %>
<% if yt.selectable? %>
<strong><%= yt.channel_title || "—" %></strong>
<strong><%= yt.channel_title || t("admin.common.dash") %></strong>
<% if yt.uses_platform_channel? %>
(canale piattaforma<%= @team.entitlements.premium_full? && @team.club.youtube_credential.blank? ? ", default senza OAuth società" : "" %>)
<%= t("admin.teams.show.youtube_platform_channel", suffix: (@team.entitlements.premium_full? && @team.club.youtube_credential.blank? ? t("admin.teams.show.youtube_platform_default_no_oauth") : "")) %>
<% else %>
(canale società)
<%= t("admin.teams.show.youtube_club_channel") %>
<% end %>
<% else %>
non pronto
<%= t("admin.teams.show.youtube_not_ready") %>
<% end %>
</p>
<% if @team.entitlements.premium_full? && @team.club.youtube_credential.blank? %>
<p class="muted">
Senza canale collegato, lapp usa il canale Match Live TV.
<%= link_to "Collega canale (pagina società)", public_club_path(@team.club) %>.
<%= t("admin.teams.show.no_channel_hint_html", link: link_to(t("admin.teams.show.link_channel"), public_club_path(@team.club))) %>
</p>
<% end %>
<h3>Partite</h3>
<h3><%= t("admin.teams.show.matches_title") %></h3>
<ul>
<% @matches.each do |m| %>
<li><%= m.opponent_name %><%= m.scheduled_at %></li>
@@ -1,10 +1,10 @@
<!DOCTYPE html>
<html lang="it">
<html lang="<%= I18n.locale %>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Token YouTube — Match Live TV</title>
<title><%= t("admin.youtube.platform_token.page_title") %></title>
<style>
body { font-family: system-ui, sans-serif; background: #0a0a0e; color: #f0f0f5; margin: 0; padding: 32px 20px; line-height: 1.5; }
.wrap { max-width: 720px; margin: 0 auto; }
@@ -17,27 +17,24 @@
</head>
<body>
<div class="wrap">
<h1>Token canale Match Live TV</h1>
<h1><%= t("admin.youtube.platform_token.title") %></h1>
<% if @channel_title.present? %>
<p>Canale: <strong><%= @channel_title %></strong></p>
<p><%= t("admin.youtube.platform_token.channel_label", title: @channel_title) %></p>
<% end %>
<% if @refresh_token.present? %>
<p class="ok">Collegamento riuscito. Copia subito questa riga:</p>
<p>Aggiungi in <code>/opt/matchlivetv/infra/.env</code>:</p>
<p class="ok"><%= t("admin.youtube.platform_token.success") %></p>
<p><%= t("admin.youtube.platform_token.env_hint_html") %></p>
<pre id="yt-token">YOUTUBE_PLATFORM_REFRESH_TOKEN=<%= @refresh_token %></pre>
<p class="muted">Non verrà mostrato di nuovo. Poi: <code>docker compose -f docker-compose.prod.yml restart rails sidekiq</code></p>
<p class="muted"><%= t("admin.youtube.platform_token.not_shown_again_html") %></p>
<% else %>
<p class="muted">
Google non ha restituito un refresh token. In
<a href="https://myaccount.google.com/permissions" target="_blank" rel="noopener">account Google → accesso app</a>
revoca «Match Live TV», poi ripeti da
<a href="/admin/youtube/platform">/admin/youtube/platform</a>.
<%= t("admin.youtube.platform_token.no_refresh_token_html") %>
</p>
<% end %>
<p><a href="/admin/">← Dashboard admin</a></p>
<p><a href="/admin/"><%= t("admin.youtube.platform_token.back_dashboard") %></a></p>
</div>
</body>
</html>
@@ -1,8 +1,11 @@
<p>Buongiorno,</p>
<p><%= t("mailers.invoice.hello") %></p>
<p>in allegato trovi la fattura <strong><%= @invoice.display_number %></strong> del <%= l(@invoice.issued_on, locale: :it, format: :long) %>
per un importo di <strong><%= @invoice.formatted_amount %></strong>, relativa alla società <strong><%= @club.billing_legal_name.presence || @club.name %></strong>.</p>
<p><%= raw t("mailers.invoice.body_html",
number: @invoice.display_number,
date: l(@invoice.issued_on, format: :long),
amount: @invoice.formatted_amount,
club: @club.billing_legal_name.presence || @club.name) %></p>
<p>Puoi scaricarla anche dalla sezione <strong>Abbonamento</strong> del tuo account Match Live TV.</p>
<p><%= raw t("mailers.invoice.also_html") %></p>
<p>Grazie,<br>Match Live TV</p>
<p><%= t("mailers.invoice.thanks") %><br><%= t("mailers.invoice.signoff") %></p>
@@ -1,9 +1,12 @@
Buongiorno,
<%= t("mailers.invoice.hello") %>
in allegato la fattura <%= @invoice.display_number %> del <%= l(@invoice.issued_on, locale: :it, format: :long) %>
(<%= @invoice.formatted_amount %>) per <%= @club.billing_legal_name.presence || @club.name %>.
<%= t("mailers.invoice.body_text",
number: @invoice.display_number,
date: l(@invoice.issued_on, format: :long),
amount: @invoice.formatted_amount,
club: @club.billing_legal_name.presence || @club.name) %>
È disponibile anche in Abbonamento sul sito Match Live TV.
<%= t("mailers.invoice.also_text") %>
Grazie,
Match Live TV
<%= t("mailers.invoice.thanks") %>
<%= t("mailers.invoice.signoff") %>
+11 -10
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<html lang="<%= current_locale %>">
<head>
<title>Match Live TV Admin</title>
<title><%= t("admin.layout.title") %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="/admin.css?v=2">
@@ -20,18 +20,19 @@
<h1>MATCH <span>LIVE</span> TV — Admin</h1>
<nav class="admin-nav">
<% if admin_logged_in? %>
<%= link_to "Dashboard", admin_root_path, class: ("active" if controller_name == "dashboard") %>
<%= link_to t("admin.layout.nav.dashboard"), admin_root_path, class: ("active" if controller_name == "dashboard") %>
<% ops_critical = Ops::Incident.critical_open.count %>
<%= link_to admin_ops_path, class: ("active" if controller_name == "ops") do %>
Ops<% if ops_critical.positive? %> <span class="admin-nav-badge"><%= ops_critical %></span><% end %>
<%= t("admin.layout.nav.ops") %><% if ops_critical.positive? %> <span class="admin-nav-badge"><%= ops_critical %></span><% end %>
<% end %>
<%= link_to "Società e squadre", admin_clubs_path, class: ("active" if controller_name.in?(%w[clubs teams club_recordings])) %>
<%= link_to "Fatturazione", admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
<%= link_to "YouTube", admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
<%= link_to "Sessions", admin_sessions_path, class: ("active" if controller_name == "sessions") %>
<%= link_to "Password", edit_admin_password_path %>
<%= button_to "Esci", admin_logout_path, method: :delete %>
<%= link_to t("admin.layout.nav.clubs"), admin_clubs_path, class: ("active" if controller_name.in?(%w[clubs teams club_recordings])) %>
<%= link_to t("admin.layout.nav.billing"), admin_billing_path, class: ("active" if controller_name.in?(%w[billing billing_invoices])) %>
<%= link_to t("admin.layout.nav.youtube"), admin_youtube_platform_path, class: ("active" if controller_name == "youtube") %>
<%= link_to t("admin.layout.nav.sessions"), admin_sessions_path, class: ("active" if controller_name == "sessions") %>
<%= link_to t("admin.layout.nav.password"), edit_admin_password_path %>
<%= button_to t("admin.layout.nav.logout"), admin_logout_path, method: :delete %>
<% end %>
<%= render "shared/language_switcher", locale_path: admin_locale_path %>
</nav>
</header>
+1 -1
View File
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer">
<link rel="stylesheet" href="/marketing.css?v=44">
</head>
<body<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<%= render "shared/cookie_banner" %>
<%= render "shared/marketing_nav" %>
<main>
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/live.css?v=26">
<%= yield :head %>
</head>
<body<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<body data-confirm-i18n='<%= raw confirm_dialog_i18n_json %>'<% if MatchLiveTv.google_analytics_configured? %> data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
<%= render "shared/cookie_banner" %>
<%= render "shared/marketing_nav" %>
<main class="live-main">
+7 -7
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="it">
<html lang="<%= html_lang %>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -34,17 +34,17 @@
<div class="wrap">
<%= link_to public_pricing_path, class: "brand" do %>Match <span>Live TV</span><% end %>
<nav>
<%= link_to "Prezzi", public_pricing_path %>
<%= link_to t("nav.pricing"), public_pricing_path %>
<% if logged_in? %>
<% if current_user.primary_club %>
· <%= link_to "La mia società", public_club_path(current_user.primary_club) %>
· <%= link_to t("nav.my_club"), public_club_path(current_user.primary_club) %>
<% elsif current_user.manageable_teams.any? %>
· <%= link_to "La mia squadra", public_team_details_path(current_user.manageable_teams.first) %>
· <%= link_to t("nav.my_team"), public_team_details_path(current_user.manageable_teams.first) %>
<% end %>
· <%= button_to "Esci", public_logout_path, method: :delete, form: { style: "display:inline" }, class: "btn btn-secondary", style: "padding:6px 12px;font-size:0.85rem" %>
· <%= button_to t("nav.logout"), public_logout_path, method: :delete, form: { style: "display:inline" }, class: "btn btn-secondary", style: "padding:6px 12px;font-size:0.85rem" %>
<% else %>
· <%= link_to "Accedi", public_login_path %>
· <%= link_to "Registrati", public_signup_path, class: "btn btn-primary", style: "padding:6px 12px;font-size:0.85rem" %>
· <%= link_to t("nav.login"), public_login_path %>
· <%= link_to t("nav.signup"), public_signup_path, class: "btn btn-primary", style: "padding:6px 12px;font-size:0.85rem" %>
<% end %>
</nav>
</div>
+2 -2
View File
@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="it">
<html lang="<%= html_lang %>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0a0a0e">
<title><%= content_for?(:title) ? yield(:title) : "Regia — Match Live TV" %></title>
<title><%= content_for?(:title) ? yield(:title) : t("regia.meta_title_fallback") %></title>
<%= render "shared/meta_tags" %>
<link rel="stylesheet" href="/regia.css?v=1">
<%= yield :head %>
@@ -1,17 +1,17 @@
<% content_for :title, "Dati di fatturazione — #{@club.name}" %>
<% content_for :title, t("billing.profile.title", name: @club.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:640px">
<h1>Dati di fatturazione</h1>
<p style="color:#aaa">Società: <strong><%= @club.name %></strong>. Inserisci i dati per lintestazione delle fatture e linvio tramite SDI o PEC.</p>
<h1><%= t("billing.profile.heading") %></h1>
<p style="color:#aaa"><%= t("billing.profile.lead", name: @club.name) %></p>
<% plan_return = params[:plan].presence_in(%w[premium_light premium_full]) %>
<% unless @club.billing_profile_complete? %>
<div class="flash alert" style="margin:16px 0">
<strong>Obbligatori per abbonarti a un piano premium.</strong>
<strong><%= t("billing.profile.required_notice") %></strong>
<%= billing_profile_incomplete_message(@club) %>
<% if plan_return %>
Dopo il salvataggio potrai procedere al pagamento di <strong><%= Plan[plan_return].name %></strong>.
<%= raw t("billing.profile.after_save_hint_html", plan: Plan[plan_return].name) %>
<% end %>
</div>
<% end %>
@@ -21,11 +21,11 @@
<%= hidden_field_tag :plan, plan_return if plan_return %>
<%= hidden_field_tag :interval, params[:interval] if params[:interval].present? %>
<%= render "shared/billing_profile_fields", record: @club %>
<%= submit_tag "Salva dati di fatturazione", class: "btn btn-primary" %>
<%= submit_tag t("billing.profile.submit"), class: "btn btn-primary" %>
<% end %>
</div>
<p style="margin-top:20px">
<%= link_to "← Abbonamento", public_club_billing_path(@club) %>
<%= link_to t("billing.profile.back_to_billing"), public_club_billing_path(@club) %>
</p>
</div>
@@ -1,23 +1,23 @@
<% content_for :title, "Programma partita — #{@club.name}" %>
<% content_for :title, t("matches.club_new.title", name: @club.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px">
<p class="results-hint" style="margin-bottom:8px">
<%= link_to "← Dirette e partite", public_live_index_path(club_id: @club.id), class: "back-link" %>
<%= link_to t("matches.club_new.back_link"), public_live_index_path(club_id: @club.id), class: "back-link" %>
</p>
<h1>Programma partita</h1>
<h1><%= t("matches.club_new.heading") %></h1>
<p style="color:#888">
<%= @club.name %>
<% if @club_admin %>
· Puoi scegliere qualsiasi squadra della società
<%= t("matches.club_new.admin_hint") %>
<% else %>
· Solo le squadre in cui sei responsabile trasmissione
<%= t("matches.club_new.staff_hint") %>
<% end %>
</p>
<div class="card" style="margin-top:16px">
<%= form_with url: public_club_matches_path(@club), method: :post, local: true do %>
<%= label_tag :team_id, "Squadra" %>
<%= label_tag :team_id, t("matches.club_new.team_label") %>
<% if @schedulable_teams.size == 1 %>
<%= hidden_field_tag :team_id, @selected_team.id %>
<p style="margin:0 0 16px;font-weight:600"><%= @selected_team.name %></p>
@@ -29,7 +29,7 @@
<%= render "public/matches/schedule_fields", match: @match %>
<%= submit_tag "Salva in programma", class: "btn btn-primary", style: "margin-top:20px" %>
<%= submit_tag t("matches.club_new.submit"), class: "btn btn-primary", style: "margin-top:20px" %>
<% end %>
</div>
</div>
@@ -1,4 +1,4 @@
<% content_for :title, "Replay#{@club.name}" %>
<% content_for :title, t("recordings.archive.heading") + " — #{@club.name}" %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap club-dashboard" style="padding-top:20px">
@@ -3,50 +3,46 @@
<% cred = club.youtube_credential %>
<section class="card club-youtube" id="youtube">
<h2>YouTube Live</h2>
<h2><%= t("club.youtube.heading") %></h2>
<% if params[:youtube] == "connected" %>
<p class="notice" style="color:#2e7d32;margin-bottom:12px">Canale YouTube della società collegato con successo.</p>
<p class="notice" style="color:#2e7d32;margin-bottom:12px"><%= t("club.youtube.connected_notice") %></p>
<% end %>
<% if entitlements.plan.youtube_mode == "matchlivetv_light" %>
<p>
Con il piano <strong>Premium Light</strong> le dirette YouTube di tutte le squadre vanno sul canale ufficiale
<strong>Match Live TV</strong>. Non serve collegare un canale della società.
<%= raw t("club.youtube.light_body_html") %>
</p>
<% if yt&.selectable? %>
<p class="muted">Canale pronto · seleziona «YouTube Live» nellapp mobile.</p>
<p class="muted"><%= t("club.youtube.light_ready") %></p>
<% else %>
<p class="muted">Canale Match Live TV in configurazione lato server. Contatta il supporto se YouTube non compare in app.</p>
<p class="muted"><%= t("club.youtube.light_configuring") %></p>
<% end %>
<% elsif entitlements.premium_full? %>
<% if cred %>
<p>
Canale collegato per <strong>tutta la società</strong>:
<strong><%= cred.channel_title.presence || cred.channel_id || "YouTube" %></strong>
<%= raw t("club.youtube.full_connected_html", channel: cred.channel_title.presence || cred.channel_id || "YouTube") %>
</p>
<p class="muted">Tutte le squadre possono trasmettere su questo canale quando scelgono il canale società in app.</p>
<%= button_to "Scollega canale", public_club_youtube_disconnect_path(club),
<p class="muted"><%= t("club.youtube.full_connected_hint") %></p>
<%= button_to t("club.youtube.disconnect"), public_club_youtube_disconnect_path(club),
method: :delete,
class: "btn btn-secondary",
form: { data: { turbo_confirm: "Scollegare il canale YouTube? Le dirette useranno il canale Match Live TV finché non ricolleghi il tuo." } } %>
form: { data: { turbo_confirm: t("club.youtube.disconnect_confirm"), confirm_kind: "delete" } } %>
<% else %>
<p>
Senza canale collegato, le dirette YouTube dallapp vanno sul canale ufficiale
<strong>Match Live TV</strong>.
Collega qui il canale YouTube della società (vale per tutte le squadre).
<%= raw t("club.youtube.full_not_connected_html") %>
</p>
<% if yt&.selectable? %>
<p class="muted">YouTube pronto in app (canale Match Live TV finché non colleghi il tuo).</p>
<p class="muted"><%= t("club.youtube.ready_in_app") %></p>
<% end %>
<% if ENV["YOUTUBE_CLIENT_ID"].present? %>
<%= link_to "Collega il canale YouTube della società", public_club_youtube_connect_path(club), class: "btn btn-primary" %>
<%= link_to t("club.youtube.connect"), public_club_youtube_connect_path(club), class: "btn btn-primary" %>
<% else %>
<p class="muted">OAuth YouTube non ancora configurato sul server.</p>
<p class="muted"><%= t("club.youtube.oauth_not_configured") %></p>
<% end %>
<% end %>
<% else %>
<p class="muted">YouTube sul canale della società richiede il piano Premium Full.</p>
<%= link_to "Vedi piani", public_prezzi_path, class: "btn btn-secondary" %>
<p class="muted"><%= t("club.youtube.requires_full") %></p>
<%= link_to t("club.youtube.see_plans"), public_prezzi_path, class: "btn btn-secondary" %>
<% end %>
</section>
@@ -1,20 +1,19 @@
<% content_for :title, "Abbonamento — #{@club.name}" %>
<% content_for :title, t("club.billing.title", name: @club.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px">
<h1>Abbonamento</h1>
<h1><%= t("billing.heading") %></h1>
<%= render "shared/club_subscription_status", club: @club, entitlements: @entitlements, subscription: @subscription, on_billing_page: true %>
<% if @subscription&.admin_comped? %>
<div class="card" style="margin-top:16px;border-color:#3d3520;background:#1a1810">
<p style="margin:0;color:#ddd">
<strong>Abbonamento omaggio Match Live TV</strong>
— piano <strong><%= @subscription.plan.name %></strong>
<strong><%= t("club.billing.comped.title") %></strong>
<%= t("club.billing.comped.plan_prefix") %> <strong><%= @subscription.plan.name %></strong>
<% if @subscription.admin_comped_reason.present? %>
(<%= @subscription.admin_comped_reason %>)
<% end %>.
Non è richiesto alcun pagamento. Per modifiche o passaggio a un piano a pagamento, contatta
<%= mail_to "info@matchlive.it", "info@matchlive.it" %>.
<%= raw t("club.billing.comped.no_payment_html", email_link: mail_to("info@matchlive.it", "info@matchlive.it")) %>
</p>
</div>
<% else %>
@@ -28,5 +27,5 @@
<%= render "shared/billing_documents", club: @club, payments: @payments %>
<p style="margin-top:24px"><%= link_to "← Società", public_club_path(@club) %></p>
<p style="margin-top:24px"><%= link_to t("club.back_to_club"), public_club_path(@club) %></p>
</div>
+7 -7
View File
@@ -1,18 +1,18 @@
<% content_for :title, "Modifica società — #{@club.name}" %>
<% content_for :title, t("club.edit.title", name: @club.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px">
<h1>Modifica società</h1>
<h1><%= t("club.edit.heading") %></h1>
<div class="card">
<%= form_with url: public_club_path(@club), method: :patch, multipart: true do %>
<%= label_tag "club[name]", "Nome società" %>
<%= label_tag "club[name]", t("club.edit.name_label") %>
<%= text_field_tag "club[name]", @club.name, required: true %>
<%= label_tag "club[sport]", "Sport principale" %>
<%= label_tag "club[sport]", t("club.sport_label") %>
<%= select_tag "club[sport]", sport_catalog_options(@club.sport) %>
<%= render "shared/branding_fields", record: @club, legend: "Logo e colori societari" %>
<%= render "shared/branding_fields", record: @club, legend: t("club.branding_legend") %>
<%= render "shared/billing_profile_fields", record: @club %>
<%= submit_tag "Salva", class: "btn btn-primary" %>
<%= submit_tag t("club.edit.save"), class: "btn btn-primary" %>
<% end %>
</div>
<p><%= link_to "← Società", public_club_path(@club) %></p>
<p><%= link_to t("club.back_to_club"), public_club_path(@club) %></p>
</div>
+20 -20
View File
@@ -1,39 +1,39 @@
<% content_for :title, "Registra società — Match Live TV" %>
<% content_for :meta_description, "Crea la società sportiva e la prima squadra su Match Live TV." %>
<% content_for :title, t("club.new.title") %>
<% content_for :meta_description, t("club.new.meta_description") %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>La tua società</h1>
<p class="auth-lead">Registra il club: potrai aggiungere più squadre (Under 13, Under 15, Serie C…).</p>
<h1><%= t("club.new.heading") %></h1>
<p class="auth-lead"><%= t("club.new.lead") %></p>
<div class="card card-wide">
<%= form_with url: public_clubs_path, multipart: true do %>
<h2 class="form-section-title">Società</h2>
<%= label_tag "club[name]", "Nome società / club" %>
<%= text_field_tag "club[name]", params.dig(:club, :name), required: true, placeholder: "es. Crazy Volley Rozzano" %>
<%= label_tag "club[sport]", "Sport principale" %>
<h2 class="form-section-title"><%= t("club.new.section_club") %></h2>
<%= label_tag "club[name]", t("club.new.name_label") %>
<%= text_field_tag "club[name]", params.dig(:club, :name), required: true, placeholder: t("club.new.name_placeholder") %>
<%= label_tag "club[sport]", t("club.sport_label") %>
<%= select_tag "club[sport]", sport_catalog_options(params.dig(:club, :sport) || "pallavolo") %>
<%= render "shared/branding_fields", record: Club.new(primary_color: "#e53935", secondary_color: "#ffffff"), legend: "Logo e colori societari" %>
<%= render "shared/branding_fields", record: Club.new(primary_color: "#e53935", secondary_color: "#ffffff"), legend: t("club.branding_legend") %>
<%= render "shared/billing_profile_fields", record: Club.new(billing_country: "IT") %>
<h2 class="form-section-title" style="margin-top:24px">Prima squadra</h2>
<%= label_tag "first_team[name]", "Nome squadra" %>
<%= text_field_tag "first_team[name]", params.dig(:first_team, :name), required: true, placeholder: "es. Under 15" %>
<h2 class="form-section-title" style="margin-top:24px"><%= t("club.new.section_first_team") %></h2>
<%= label_tag "first_team[name]", t("club.new.first_team_name_label") %>
<%= text_field_tag "first_team[name]", params.dig(:first_team, :name), required: true, placeholder: t("club.new.first_team_name_placeholder") %>
<%= label_tag :plan, "Piano iniziale (per tutta la società)" %>
<%= label_tag :plan, t("club.new.plan_label") %>
<%= select_tag :plan, options_for_select([
["Free — 1 responsabile trasmissione per squadra, 1 live", "free"],
["Premium Light", "premium_light"],
["Premium Full", "premium_full"]
[t("club.new.plan_free"), "free"],
[t("club.new.plan_light"), "premium_light"],
[t("club.new.plan_full"), "premium_full"]
], params[:plan] || "free"), id: "club_plan_select" %>
<div id="club_plan_interval" style="margin-top:10px">
<%= label_tag :interval, "Fatturazione premium" %>
<%= label_tag :interval, t("club.new.interval_label") %>
<%= select_tag :interval, options_for_select([
["Annuale — €40/anno (Light) o €200/anno (Full)", "yearly"],
["Mensile — €5/mese (Light) o €20/mese (Full)", "monthly"]
[t("club.new.interval_yearly"), "yearly"],
[t("club.new.interval_monthly"), "monthly"]
], params[:interval] || "yearly") %>
</div>
<%= render "shared/stripe_secure_payment", compact: true %>
<%= submit_tag "Crea società", class: "btn btn-primary" %>
<%= submit_tag t("club.new.submit"), class: "btn btn-primary" %>
<% end %>
</div>
</section>
+23 -23
View File
@@ -1,4 +1,4 @@
<% content_for :title, "#{@club.name} — Società" %>
<% content_for :title, t("club.dashboard.title", name: @club.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap club-dashboard" style="padding-top:20px">
@@ -10,20 +10,20 @@
<h1><%= @club.name %></h1>
<% if @entitlements %>
<p class="club-meta">
Piano: <strong><%= @entitlements.plan.name %></strong>
· Squadre: <strong><%= @teams.size %></strong>
<%= t("club.dashboard.plan_label") %> <strong><%= @entitlements.plan.name %></strong>
· <%= t("club.dashboard.teams_count_label") %> <strong><%= @teams.size %></strong>
</p>
<% end %>
</div>
</header>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin:20px 0">
<%= link_to "Modifica società", public_edit_club_path(@club), class: "btn btn-secondary" %>
<%= link_to "Abbonamento", public_club_billing_path(@club), class: "btn btn-primary" %>
<%= link_to "Nuova squadra", public_new_club_team_path(@club), class: "btn btn-secondary" %>
<%= link_to "Dirette live", public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
<%= link_to t("club.dashboard.edit_club"), public_edit_club_path(@club), class: "btn btn-secondary" %>
<%= link_to t("club.dashboard.subscription"), public_club_billing_path(@club), class: "btn btn-primary" %>
<%= link_to t("club.dashboard.new_team"), public_new_club_team_path(@club), class: "btn btn-secondary" %>
<%= link_to t("club.dashboard.live_streams"), public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
<% if @entitlements&.can_access_recordings? %>
<%= link_to "Archivio Replay", public_club_recordings_path(@club), class: "btn btn-secondary" %>
<%= link_to t("club.dashboard.replay_archive"), public_club_recordings_path(@club), class: "btn btn-secondary" %>
<% end %>
</div>
@@ -33,32 +33,32 @@
entitlements_team: @entitlements_team %>
<% if @replay_stats %>
<h2>Replay</h2>
<h2><%= t("club.dashboard.replay_heading") %></h2>
<div class="card" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:16px;padding:16px;margin-bottom:24px">
<div>
<div style="font-size:1.6rem;font-weight:700"><%= @replay_stats[:available_count] %></div>
<div class="muted">disponibili</div>
<div class="muted"><%= t("club.dashboard.available") %></div>
</div>
<div>
<div style="font-size:1.6rem;font-weight:700"><%= @replay_stats[:expiring_soon_count] %></div>
<div class="muted">in scadenza (7 gg)</div>
<div class="muted"><%= t("club.dashboard.expiring_soon") %></div>
</div>
<div>
<div style="font-size:1.6rem;font-weight:700"><%= number_to_human_size(@replay_stats[:total_bytes]) %></div>
<div class="muted">spazio occupato</div>
<div class="muted"><%= t("club.dashboard.space_used") %></div>
</div>
<div>
<div style="font-size:1.6rem;font-weight:700"><%= @replay_stats[:total_views] %></div>
<div class="muted">visualizzazioni</div>
<div class="muted"><%= t("club.dashboard.views") %></div>
</div>
</div>
<% end %>
<h2>Squadre</h2>
<h2><%= t("club.dashboard.teams_heading") %></h2>
<div class="card">
<% if @teams.any? %>
<table class="data">
<thead><tr><th>Squadra</th><th>Branding</th><th></th></tr></thead>
<thead><tr><th><%= t("club.dashboard.team_col") %></th><th><%= t("club.dashboard.branding_col") %></th><th></th></tr></thead>
<tbody>
<% @teams.each do |team| %>
<tr>
@@ -68,28 +68,28 @@
</td>
<td style="color:#888;font-size:0.88rem">
<% if team.logo_url.present? || team.logo_file.attached? || team.primary_color.present? %>
Personalizzato
<%= t("club.dashboard.branding_custom") %>
<% else %>
Eredita dalla società
<%= t("club.dashboard.branding_inherited") %>
<% end %>
</td>
<td style="white-space:nowrap">
<%= link_to "Pagina pubblica", public_team_page_path(team.slug), target: "_blank", rel: "noopener" %>
<%= link_to t("club.dashboard.public_page"), public_team_page_path(team.slug), target: "_blank", rel: "noopener" %>
·
<%= link_to "Dettagli", public_team_details_path(team) %>
<%= link_to t("club.dashboard.details"), public_team_details_path(team) %>
·
<%= link_to "Partite", public_team_matches_path(team) %>
<%= link_to t("club.dashboard.matches"), public_team_matches_path(team) %>
·
<%= link_to "Modifica", public_edit_team_path(team) %>
<%= link_to t("club.dashboard.edit"), public_edit_team_path(team) %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p style="color:#888">Nessuna squadra. <%= link_to "Aggiungi la prima", public_new_club_team_path(@club) %>.</p>
<p style="color:#888"><%= raw t("club.dashboard.no_teams_html", link: link_to(t("club.dashboard.add_first_team"), public_new_club_team_path(@club))) %></p>
<% end %>
</div>
<p style="color:#888;margin-top:24px">Ogni squadra ha i propri responsabili trasmissione. Il punteggio da un altro telefono usa il link regia (senza account). Labbonamento vale per tutta la società.</p>
<p style="color:#888;margin-top:24px"><%= t("club.dashboard.footer_note") %></p>
</div>
@@ -1,21 +1,26 @@
<% content_for :title, "Invito squadra — Match Live TV" %>
<% content_for :meta_description, "Accetta l'invito per entrare nello staff della squadra su Match Live TV." %>
<% content_for :title, t("auth.invitation.meta_title") %>
<% content_for :meta_description, t("auth.invitation.meta_description") %>
<% content_for :robots, "noindex, nofollow" %>
<div class="card" style="max-width:480px">
<h1>Accesso staff — <%= @invitation.team.name %></h1>
<p>Sei stato aggiunto come <strong>responsabile trasmissione</strong> (<%= @invitation.email %>).</p>
<h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1>
<p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p>
<p class="muted" style="font-size:0.9rem;margin-bottom:16px">
Nellapp Match Live TV accedi con <strong><%= @invitation.email %></strong>, accetta linvito e collega il tuo canale YouTube.
<%= raw t("auth.invitation.instructions_html", email: @invitation.email) %>
</p>
<% if logged_in? %>
<%= button_to "Accetta invito", public_invitation_path(token: @token), method: :post, class: "btn btn-primary" %>
<%= button_to t("auth.invitation.accept"), public_invitation_path(token: @token), method: :post, class: "btn btn-primary" %>
<% else %>
<p><%= link_to "Accedi", public_login_path %> o <%= link_to "registrati", public_signup_path %> con <%= @invitation.email %>.</p>
<%= button_to "Accetta (se già loggato)", public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %>
<p><%= raw t(
"auth.invitation.login_or_signup_html",
login_link: link_to(t("auth.invitation.login_link"), public_login_path),
signup_link: link_to(t("auth.invitation.signup_link"), public_signup_path),
email: @invitation.email
) %></p>
<%= button_to t("auth.invitation.accept_if_logged_in"), public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %>
<% end %>
<p style="margin-top:16px;font-size:0.88rem;color:#888">
App mobile:
<a href="matchlivetv://join/<%= @token %>">Apri invito nellapp</a>
<%= t("auth.invitation.mobile_app_label") %>
<a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a>
</p>
</div>
@@ -5,7 +5,7 @@
badge_class = local_assigns[:badge_class]
unless badge_label
badge_label = closed ? "Terminata" : (session.paused? ? "In pausa" : (on_air ? "LIVE" : "In attesa"))
badge_label = closed ? t("live.overlays.badge_ended") : (session.paused? ? t("live.index.badge_paused") : (on_air ? t("live.index.badge_live") : t("live.overlays.badge_waiting")))
end
unless badge_class
+51 -51
View File
@@ -1,33 +1,33 @@
<% content_for :title, "Dirette live partite giovanili in corso — Match Live TV" %>
<% content_for :meta_description, "Guarda le dirette live delle partite giovanili ora in corso e le prossime gare programmate. Cerca la tua squadra e apri il link senza installare app." %>
<% content_for :title, t("live.index.meta_title") %>
<% content_for :meta_description, t("live.index.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_live_index_path) %>
<% live_index_params = @club ? { club_id: @club.id } : {} %>
<div class="wrap">
<% if @club %>
<p class="results-hint" style="margin-bottom:8px">
<%= link_to "← #{@club.name}", public_club_path(@club), class: "back-link" %>
<%= link_to t("live.index.club_back_link", name: @club.name), public_club_path(@club), class: "back-link" %>
</p>
<h1>Dirette e partite — <%= @club.name %></h1>
<h1><%= t("live.index.club_title", name: @club.name) %></h1>
<p class="results-hint">
Solo le squadre di questa società: dirette in corso e partite programmate da app o sito.
<%= t("live.index.club_hint") %>
</p>
<div class="live-actions" style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px">
<%= link_to "Replay di #{@club.name}", public_replay_index_path(club_id: @club.id), class: "btn btn-secondary" %>
<%= link_to t("live.index.club_replay_link", name: @club.name), public_replay_index_path(club_id: @club.id), class: "btn btn-secondary" %>
</div>
<% else %>
<h1>Dirette e partite in programma</h1>
<h1><%= t("live.index.default_title") %></h1>
<p class="results-hint">
Cerca per società, squadra, avversario o luogo: trovi le dirette attive e le partite programmate.
<%= t("live.index.default_hint") %>
</p>
<div class="live-actions" style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:20px">
<%= link_to "Live passate — archivio replay", public_replay_index_path, class: "btn btn-secondary" %>
<%= link_to t("live.index.replay_archive_link"), public_replay_index_path, class: "btn btn-secondary" %>
</div>
<% end %>
<% if @club && @can_schedule_match %>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px">
<%= link_to "Programma partita", public_new_club_match_path(@club), class: "btn btn-primary" %>
<%= link_to t("live.index.schedule_match_link"), public_new_club_match_path(@club), class: "btn btn-primary" %>
</div>
<% end %>
@@ -39,25 +39,25 @@
type="search"
name="q"
value="<%= @query %>"
placeholder="<%= @club ? "Cerca società, squadra, avversario o luogo…" : "Es. Crazy Volley, Serie D, avversario…" %>"
aria-label="Cerca squadra"
placeholder="<%= @club ? t("live.index.search_placeholder_club") : t("live.index.search_placeholder_default") %>"
aria-label="<%= t("live.index.search_aria_label") %>"
autocomplete="off"
/>
<button type="submit" class="btn btn-primary">Cerca</button>
<button type="submit" class="btn btn-primary"><%= t("live.index.search_button") %></button>
<% if @query.present? %>
<%= link_to "Azzera", public_live_index_path(live_index_params), class: "btn btn-secondary" %>
<%= link_to t("live.index.reset_link"), public_live_index_path(live_index_params), class: "btn btn-secondary" %>
<% end %>
<% end %>
<% if @query.present? %>
<% club_suffix = @club ? t("live.index.results_hint_club_suffix", name: @club.name) : "" %>
<p class="results-hint">
Risultati per «<%= h @query %>»<%= @club ? " in #{@club.name}" : "" %>:
<%= @sessions.size %> in diretta · <%= @upcoming_matches.size %> in programma
<%= t("live.index.results_hint", query: @query, club_suffix: club_suffix, live_count: @sessions.size, upcoming_count: @upcoming_matches.size) %>
</p>
<% end %>
<% if @sessions.any? %>
<h2 class="section-heading">In diretta adesso</h2>
<h2 class="section-heading"><%= t("live.index.section_live_now") %></h2>
<div class="live-grid">
<% @sessions.each do |session| %>
<% match = session.match %>
@@ -72,33 +72,33 @@
<p class="card-score">
<span class="card-sets"><%= live_score_sets_label(session.score_state) %></span>
<% if live_score_partials_label(session.score_state).present? %>
<span class="card-partials">Parziali: <%= live_score_partials_label(session.score_state) %></span>
<span class="card-partials"><%= t("score.partials_prefix", value: live_score_partials_label(session.score_state)) %></span>
<% end %>
<span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span>
</p>
<% end %>
<div class="badges">
<% if on_air %>
<span class="badge badge-on-air">In onda</span>
<span class="badge badge-on-air"><%= t("live.index.badge_on_air") %></span>
<% elsif session.status == "live" %>
<span class="badge badge-live">Live</span>
<span class="badge badge-live"><%= t("live.index.badge_live") %></span>
<% elsif session.status == "paused" %>
<span class="badge badge-connecting">In pausa</span>
<span class="badge badge-connecting"><%= t("live.index.badge_paused") %></span>
<% elsif session.status == "reconnecting" %>
<span class="badge badge-connecting">Riconnessione</span>
<span class="badge badge-connecting"><%= t("live.index.badge_reconnecting") %></span>
<% else %>
<span class="badge badge-wait">In avvio</span>
<span class="badge badge-wait"><%= t("live.index.badge_starting") %></span>
<% end %>
</div>
<%= link_to "Guarda diretta →", public_live_path(session), class: "btn-watch" %>
<%= link_to t("live.index.watch_link"), public_live_path(session), class: "btn-watch" %>
</article>
<% end %>
</div>
<% end %>
<% if @upcoming_matches.any? %>
<h2 class="section-heading<%= ' section-heading--spaced' if @sessions.any? %>">Prossime partite</h2>
<p class="results-hint">Programmate da app o sito: la diretta partirà quando lo staff avvierà la trasmissione.</p>
<h2 class="section-heading<%= ' section-heading--spaced' if @sessions.any? %>"><%= t("live.index.section_upcoming") %></h2>
<p class="results-hint"><%= t("live.index.upcoming_hint") %></p>
<div class="live-grid upcoming-grid">
<% @upcoming_matches.each do |match| %>
<article class="live-card live-card--upcoming">
@@ -111,7 +111,7 @@
<%= live_scheduled_relative(match.scheduled_at) %>
</p>
<div class="badges">
<span class="badge badge-scheduled">In programma</span>
<span class="badge badge-scheduled"><%= t("live.index.badge_scheduled") %></span>
</div>
</article>
<% end %>
@@ -121,59 +121,59 @@
<% if @sessions.empty? %>
<% if @upcoming_matches.any? %>
<div class="empty-state empty-state--soft">
<p><strong>Nessuna diretta in questo momento.</strong></p>
<p>Torna allorario indicato: quando la squadra avvierà lo streaming, la partita comparirà in «In diretta adesso».</p>
<p><strong><%= t("live.index.empty_soft_title") %></strong></p>
<p><%= t("live.index.empty_soft_body") %></p>
</div>
<% elsif @club %>
<div class="empty-state">
<p><strong>Nessuna diretta o partita in programma per <%= @club.name %>.</strong></p>
<p><strong><%= t("live.index.empty_club_title", name: @club.name) %></strong></p>
<p>
<% if @can_schedule_match %>
<%= link_to "Programma la prima partita", public_new_club_match_path(@club), class: "btn btn-primary" %>
oppure dallapp Match Live TV.
<%= link_to t("live.index.empty_club_schedule_link"), public_new_club_match_path(@club), class: "btn btn-primary" %>
<%= t("live.index.empty_club_schedule_suffix") %>
<% elsif @club.teams.any? %>
Chiedi al titolare della società o a un responsabile trasmissione di programmare una gara.
<%= t("live.index.empty_club_ask_staff") %>
<% else %>
Aggiungi una squadra dalla
<%= link_to "pagina società", public_club_path(@club) %>
e programma la prima partita.
<%= raw t(
"live.index.empty_club_add_team_html",
club_link: link_to(t("live.index.empty_club_page_link"), public_club_path(@club))
) %>
<% end %>
</p>
<p style="margin-top:16px">
<%= link_to "← Torna alla società", public_club_path(@club), class: "btn btn-secondary" %>
<%= link_to t("live.index.empty_club_back_link"), public_club_path(@club), class: "btn btn-secondary" %>
</p>
</div>
<% else %>
<div class="empty-hero">
<div class="empty-hero-copy">
<h2>Nessuna diretta adesso — ma puoi già organizzarti</h2>
<h2><%= t("live.index.empty_hero_title") %></h2>
<p>
Se la tua squadra usa Match Live TV, dal telefono si programma la partita con data e ora.
Genitori e parenti trovano qui la diretta quando il fischio dinizio è vicino.
<%= t("live.index.empty_hero_body") %>
</p>
<ul class="empty-hero-list">
<li>Programma la gara dallapp (senza avviare subito la diretta)</li>
<li>La partita compare in «Prossime partite» con orario e campo</li>
<li>Lo streaming parte solo quando premi avvio in palestra</li>
<li><%= t("live.index.empty_hero_list_item1") %></li>
<li><%= t("live.index.empty_hero_list_item2") %></li>
<li><%= t("live.index.empty_hero_list_item3") %></li>
</ul>
<p class="empty-hero-cta">
<%= link_to "Registra la tua squadra", public_signup_path, class: "btn btn-primary" %>
<%= link_to "Scopri come funziona", public_features_path, class: "btn btn-secondary" %>
<%= link_to t("live.index.empty_hero_cta_signup"), public_signup_path, class: "btn btn-primary" %>
<%= link_to t("live.index.empty_hero_cta_features"), public_features_path, class: "btn btn-secondary" %>
</p>
</div>
<aside class="demo-live-card" aria-label="Esempio di diretta attiva">
<p class="demo-label">Esempio — così appare una diretta attiva</p>
<aside class="demo-live-card" aria-label="<%= t("live.index.demo_aria_label") %>">
<p class="demo-label"><%= t("live.index.demo_label") %></p>
<article class="live-card live-card--demo">
<h3>Tigers Volley vs ASD Eagles</h3>
<p class="meta">PalaTigers · Match Live TV</p>
<p class="meta"><%= t("live.index.demo_meta") %></p>
<p class="card-score">
<span class="card-sets">Set 2 · Set vinti 1-0</span>
<span class="card-sets"><%= t("live.index.demo_sets") %></span>
<span class="card-points">18 - 16</span>
</p>
<div class="badges">
<span class="badge badge-on-air">In onda</span>
<span class="badge badge-on-air"><%= t("live.index.badge_on_air") %></span>
</div>
<span class="btn-watch btn-watch--demo">Guarda diretta →</span>
<span class="btn-watch btn-watch--demo"><%= t("live.index.watch_link") %></span>
</article>
</aside>
</div>
+32 -26
View File
@@ -1,6 +1,12 @@
<% club_name = @match.team.club&.name %>
<% content_for :title do %><%= club_name %> · <%= @match.team.name %> vs <%= @match.opponent_name %> — Diretta live<% end %>
<% content_for :meta_description do %>Segui in diretta live <%= club_name %><%= @match.team.name %> vs <%= @match.opponent_name %><% if @match.location.present? %><%= @match.location %><% end %>. Guarda dal browser senza installare app.<% end %>
<% content_for :title do %><%= t("live.show.title", club_name: club_name, team_name: @match.team.name, opponent_name: @match.opponent_name) %><% end %>
<% content_for :meta_description do %><%= t(
"live.show.meta_description",
club_name: club_name,
team_name: @match.team.name,
opponent_name: @match.opponent_name,
location: @match.location.present? ? t("live.show.meta_description_location", location: @match.location) : ""
) %><% end %>
<% content_for :robots, @session.publicly_listed? ? "index, follow" : "noindex, nofollow" %>
<% content_for :head do %>
@@ -10,45 +16,45 @@
<% end %>
<div class="wrap">
<%= link_to "← Tutte le dirette", public_live_index_path, class: "back-link" %>
<%= link_to t("live.show.back_to_all"), public_live_index_path, class: "back-link" %>
<%= live_match_page_heading(@match) %>
<% if @stream_closed %>
<div id="stream-ended" class="stream-ended" role="status" aria-live="polite">
<div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2>Diretta terminata</h2>
<p>Lo streaming di questa partita è stato chiuso.</p>
<h2><%= t("live.show.stream_ended_title") %></h2>
<p><%= t("live.show.stream_ended_body") %></p>
<% if @session.ended_at %>
<p class="stream-ended-meta">Chiusa il <%= l_local(@session.ended_at) %></p>
<p class="stream-ended-meta"><%= t("live.show.stream_ended_closed_at", date: l_local(@session.ended_at)) %></p>
<% end %>
<p class="stream-ended-hint">Il punteggio è visibile nel video se la diretta era ancora in corso.</p>
<%= link_to "Tutte le dirette", public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
<p class="stream-ended-hint"><%= t("live.show.stream_ended_hint") %></p>
<%= link_to t("live.show.all_live_link"), public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
</div>
<% elsif @session.platform == "youtube" %>
<div class="stream-ended youtube-live-cta" role="status" aria-live="polite">
<div class="stream-ended-icon" aria-hidden="true"></div>
<h2>Diretta su YouTube</h2>
<h2><%= t("live.show.youtube_title") %></h2>
<% if @session.youtube_watch_url.present? %>
<p>Questa partita è trasmessa sul canale YouTube Match Live TV, non sul player del sito.</p>
<%= link_to "Guarda su YouTube", @session.youtube_watch_url, class: "btn stream-ended-btn", target: "_blank", rel: "noopener" %>
<p><%= t("live.show.youtube_available_body") %></p>
<%= link_to t("live.show.youtube_watch_link"), @session.youtube_watch_url, class: "btn stream-ended-btn", target: "_blank", rel: "noopener" %>
<% else %>
<p>Stiamo preparando la diretta sul canale YouTube. Aggiorna tra qualche secondo.</p>
<p><%= t("live.show.youtube_preparing_body") %></p>
<meta http-equiv="refresh" content="8">
<% end %>
<p class="stream-ended-hint">Condividi solo il link YouTube con tifosi e società.</p>
<p class="stream-ended-hint"><%= t("live.show.youtube_hint") %></p>
</div>
<% else %>
<div class="live-player-wrap">
<video id="player" controls playsinline muted autoplay poster="/images/copertina-canale.png"></video>
<%= render "public/live/player_overlays", match: @match, session: @session %>
<button type="button" id="play-hint" class="live-play-hint" hidden>
▶ Avvia la diretta
<%= t("live.show.play_hint") %>
</button>
</div>
<p id="offline-msg" class="live-status-msg" hidden>La diretta non è ancora disponibile. Si aggiorna automaticamente quando torna in onda.</p>
<p id="awaiting-msg" class="live-status-msg" hidden>In attesa del segnale dal telefono — apri la camera nell'app e verifica che la diretta sia avviata (non in pausa).</p>
<p id="paused-msg" class="live-status-msg" hidden>Trasmissione in pausa — il flusso continua (copertina server).</p>
<p id="offline-msg" class="live-status-msg" hidden><%= t("live.show.offline_msg") %></p>
<p id="awaiting-msg" class="live-status-msg" hidden><%= t("live.show.awaiting_msg") %></p>
<p id="paused-msg" class="live-status-msg" hidden><%= t("live.show.paused_msg") %></p>
<script>
const sessionId = "<%= @session.id %>";
@@ -63,19 +69,19 @@
function syncStreamBadge(data) {
if (!streamBadge) return;
if (data.stream_closed) {
streamBadge.textContent = "Terminata";
streamBadge.textContent = "<%= j t("live.overlays.badge_ended") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-ended";
return;
}
const paused = !!(data.paused || data.status === "paused");
if (paused) {
streamBadge.textContent = "In pausa";
streamBadge.textContent = "<%= j t("live.index.badge_paused") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
} else if (data.on_air) {
streamBadge.textContent = "LIVE";
streamBadge.textContent = "<%= j t("live.index.badge_live") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-live";
} else {
streamBadge.textContent = "In attesa";
streamBadge.textContent = "<%= j t("live.overlays.badge_waiting") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
}
}
@@ -158,10 +164,10 @@
panel.setAttribute("role", "status");
panel.innerHTML = `
<div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2>Diretta terminata</h2>
<p>Lo streaming di questa partita è stato chiuso.</p>
<p class="stream-ended-hint">Ricarica la pagina per lultimo stato registrato.</p>
<a href="<%= public_live_index_path %>" class="btn btn-secondary stream-ended-btn">Tutte le dirette</a>
<h2><%= j t("live.show.stream_ended_title") %></h2>
<p><%= j t("live.show.stream_ended_body") %></p>
<p class="stream-ended-hint"><%= j t("live.show.stream_ended_reload_hint") %></p>
<a href="<%= public_live_index_path %>" class="btn btn-secondary stream-ended-btn"><%= j t("live.show.all_live_link") %></a>
`;
video.parentNode.insertBefore(panel, video.nextSibling);
}
@@ -277,7 +283,7 @@
offlineMsg.hidden = true;
if (!publisherOnline && showingCover && !paused) {
awaitingMsg.hidden = false;
awaitingMsg.textContent = "In riconnessione - in attesa del video dall'evento";
awaitingMsg.textContent = "<%= j t("live.show.js_reconnecting_awaiting") %>";
} else {
awaitingMsg.hidden = !awaitingSignal;
}
@@ -1,13 +1,13 @@
<%= label_tag "match[opponent_name]", "Avversario" %>
<%= text_field_tag "match[opponent_name]", match.opponent_name, required: true, placeholder: "es. Volley Milano" %>
<%= label_tag "match[opponent_name]", t("matches.opponent_label") %>
<%= text_field_tag "match[opponent_name]", match.opponent_name, required: true, placeholder: t("matches.opponent_placeholder") %>
<%= label_tag "match[location]", "Luogo (opzionale)" %>
<%= text_field_tag "match[location]", match.location, placeholder: "Palestra, città" %>
<%= label_tag "match[location]", t("matches.location_label") %>
<%= text_field_tag "match[location]", match.location, placeholder: t("matches.location_placeholder") %>
<%= label_tag "match[scheduled_at]", "Data e ora" %>
<%= label_tag "match[scheduled_at]", t("matches.datetime_label") %>
<% scheduled_value = match.scheduled_at&.in_time_zone&.strftime("%Y-%m-%dT%H:%M") %>
<%= datetime_local_field_tag "match[scheduled_at]", scheduled_value, required: true %>
<p style="color:#888;font-size:0.88rem;margin:12px 0 0">
La diretta non parte da sola: comparirà in app e sul sito. Avvierai lo streaming dal telefono quando sei in campo.
<%= t("matches.form_hint") %>
</p>
@@ -1,14 +1,14 @@
<% content_for :title, "Modifica partita — #{@team.name}" %>
<% content_for :title, t("matches.edit.title", name: @team.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px">
<h1>Modifica partita</h1>
<h1><%= t("matches.edit.heading") %></h1>
<p style="color:#888"><%= @team.name %> vs <strong><%= @match.opponent_name %></strong></p>
<div class="card" style="margin-top:16px">
<%= render "form",
form_url: public_team_match_path(@team, @match),
form_method: :patch,
submit_label: "Salva modifiche" %>
submit_label: t("matches.edit.submit") %>
</div>
<p style="margin-top:16px"><%= link_to "← Elenco partite", public_team_matches_path(@team) %></p>
<p style="margin-top:16px"><%= link_to t("matches.back_to_list"), public_team_matches_path(@team) %></p>
</div>
+16 -18
View File
@@ -1,4 +1,4 @@
<% content_for :title, "Partite — #{@team.name}" %>
<% content_for :title, t("matches.index.title", name: @team.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:720px">
@@ -7,17 +7,16 @@
</nav>
<header style="margin:16px 0 20px">
<h1>Partite — <%= @team.name %></h1>
<h1><%= t("matches.index.heading", name: @team.name) %></h1>
<p style="color:#888;margin-top:8px">
Programma qui da PC data, avversario e luogo. Quando sei in palestra apri lapp Match Live TV,
seleziona questa squadra e avvia la diretta sulla partita.
<%= t("matches.index.lead") %>
</p>
</header>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:20px">
<%= link_to "Programma partita", new_public_team_match_path(@team), class: "btn btn-primary" %>
<%= link_to "Dettagli squadra", public_team_details_path(@team), class: "btn btn-secondary" %>
<%= link_to "Dirette e programma", public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
<%= link_to t("matches.index.schedule_match"), new_public_team_match_path(@team), class: "btn btn-primary" %>
<%= link_to t("matches.index.team_details"), public_team_details_path(@team), class: "btn btn-secondary" %>
<%= link_to t("matches.index.live_and_schedule"), public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
</div>
<div class="card">
@@ -25,10 +24,10 @@
<table class="data">
<thead>
<tr>
<th>Avversario</th>
<th>Data e ora</th>
<th>Luogo</th>
<th>Stato</th>
<th><%= t("matches.index.col_opponent") %></th>
<th><%= t("matches.index.col_datetime") %></th>
<th><%= t("matches.index.col_location") %></th>
<th><%= t("matches.index.col_status") %></th>
<th></th>
</tr>
</thead>
@@ -40,25 +39,25 @@
<% if match.scheduled_at %>
<%= l_local(match.scheduled_at) %>
<% else %>
<span style="color:#888">Senza orario</span>
<span style="color:#888"><%= t("matches.index.no_time") %></span>
<% end %>
</td>
<td><%= match.location.presence || "—" %></td>
<td>
<% label = match.public_status_label %>
<% if label == "In corso / da riprendere" %>
<% if match.active_stream_session %>
<span style="color:#e53935;font-weight:600"><%= label %></span>
<% else %>
<%= label %>
<% end %>
</td>
<td style="white-space:nowrap">
<%= link_to "Modifica", edit_public_team_match_path(@team, match) %>
<%= link_to t("matches.index.edit"), edit_public_team_match_path(@team, match) %>
<% if match.deletable? %>
·
<%= button_to "Elimina", public_team_match_path(@team, match),
<%= button_to t("matches.index.delete"), public_team_match_path(@team, match),
method: :delete,
form: { data: { turbo_confirm: "Eliminare questa partita?" } },
form: { data: { turbo_confirm: t("matches.index.delete_confirm"), confirm_kind: "delete" } },
class: "btn btn-secondary",
style: "padding:4px 10px;font-size:0.8rem;display:inline" %>
<% end %>
@@ -69,8 +68,7 @@
</table>
<% else %>
<p style="color:#888;margin:0">
Nessuna partita in calendario.
<%= link_to "Programma la prima", new_public_team_match_path(@team) %>.
<%= raw t("matches.index.empty_html", link: link_to(t("matches.index.schedule_first"), new_public_team_match_path(@team))) %>
</p>
<% end %>
</div>
@@ -1,14 +1,14 @@
<% content_for :title, "Programma partita — #{@team.name}" %>
<% content_for :title, t("matches.new.title", name: @team.name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px">
<h1>Programma partita</h1>
<h1><%= t("matches.new.heading") %></h1>
<p style="color:#888"><%= @club.name %> · <strong><%= @team.name %></strong></p>
<div class="card" style="margin-top:16px">
<%= render "form",
form_url: public_team_matches_path(@team),
form_method: :post,
submit_label: "Salva in programma" %>
submit_label: t("matches.new.submit") %>
</div>
<p style="margin-top:16px"><%= link_to "← Elenco partite", public_team_matches_path(@team) %></p>
<p style="margin-top:16px"><%= link_to t("matches.back_to_list"), public_team_matches_path(@team) %></p>
</div>
+65 -69
View File
@@ -1,150 +1,146 @@
<% content_for :title, "Cookie policy — Match Live TV" %>
<% content_for :meta_description, "Cookie policy di Match Live TV: cookie tecnici, sessione, Google Analytics e gestione del consenso." %>
<% content_for :title, t("legal.cookies.title") %>
<% content_for :meta_description, t("legal.cookies.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_cookies_path) %>
<div class="wrap legal-doc">
<h1>Cookie policy</h1>
<h1><%= t("legal.cookies.h1") %></h1>
<p class="legal-meta">
Ultimo aggiornamento: <%= cookie_policy_last_updated %>
· Vedi anche <%= link_to "informativa privacy", public_privacy_path %>
<%= raw t(
"legal.cookies.meta_html",
date: cookie_policy_last_updated,
privacy_link: link_to(t("legal.cookies.privacy_link_text"), public_privacy_path)
) %>
</p>
<section>
<h2>1. Cosa sono i cookie</h2>
<h2><%= t("legal.cookies.s1_title") %></h2>
<p>
I cookie sono piccoli file di testo che i siti salvano sul dispositivo del visitatore.
Servono a far funzionare il sito, ricordare preferenze o, previo consenso, analizzare luso del servizio.
<%= t("legal.cookies.s1_body") %>
</p>
</section>
<section>
<h2>2. Titolare</h2>
<h2><%= t("legal.cookies.s2_title") %></h2>
<p>
<strong><%= MatchLiveTv.privacy_controller_name %></strong>
<%= MatchLiveTv.privacy_controller_address %>
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>
<% if MatchLiveTv.privacy_controller_vat.present? %>
· P. IVA <%= MatchLiveTv.privacy_controller_vat %>
· <%= t("legal.cookies.s2_vat_label") %> <%= MatchLiveTv.privacy_controller_vat %>
<% end %>
</p>
</section>
<section>
<h2>3. Come gestiamo il consenso</h2>
<h2><%= t("legal.cookies.s3_title") %></h2>
<p>
Al primo accesso mostriamo un banner che ti permette di accettare tutti i cookie,
usare <strong>solo quelli necessari</strong> o aprire questa pagina per maggiori dettagli.
Puoi modificare la scelta in qualsiasi momento con il link
<strong>«Gestisci cookie»</strong> nel footer.
<%= raw t("legal.cookies.s3_p1_html") %>
</p>
<p>
La preferenza viene memorizzata in un cookie tecnico di prima parte
(<code>mltv_cookie_consent</code>) e in <code>localStorage</code> del browser.
<%= raw t("legal.cookies.s3_p2_html") %>
</p>
</section>
<section>
<h2>4. Cookie e tecnologie che utilizziamo</h2>
<h2><%= t("legal.cookies.s4_title") %></h2>
<h3>4.1 Strettamente necessari (sempre attivi)</h3>
<p>Non richiedono consenso perché indispensabili al funzionamento del sito e dellarea riservata.</p>
<h3><%= t("legal.cookies.s4_1_title") %></h3>
<p><%= t("legal.cookies.s4_1_intro") %></p>
<table class="legal-table">
<thead>
<tr><th>Nome / tipo</th><th>Finalità</th><th>Durata</th><th>Fornitore</th></tr>
<tr><th><%= t("legal.cookies.table_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
</thead>
<tbody>
<tr>
<td><code>_app_session</code> (cookie di sessione)</td>
<td>Mantiene laccesso dellutente registrato, protezione CSRF, preferenze di navigazione sicura</td>
<td>Sessione / fino a chiusura browser</td>
<td>Match Live TV (prima parte)</td>
<td><%= raw t("legal.cookies.s4_1_row1_name_html") %></td>
<td><%= t("legal.cookies.s4_1_row1_purpose") %></td>
<td><%= t("legal.cookies.s4_1_row1_duration") %></td>
<td><%= t("legal.cookies.s4_1_row1_provider") %></td>
</tr>
<tr>
<td><code>mltv_cookie_consent</code></td>
<td>Memorizza le scelte sul banner cookie (necessari / analytics)</td>
<td>12 mesi</td>
<td>Match Live TV (prima parte)</td>
<td><%= raw t("legal.cookies.s4_1_row2_name_html") %></td>
<td><%= t("legal.cookies.s4_1_row2_purpose") %></td>
<td><%= t("legal.cookies.s4_1_row2_duration") %></td>
<td><%= t("legal.cookies.s4_1_row2_provider") %></td>
</tr>
</tbody>
</table>
<h3>4.2 Analytics (solo con il tuo consenso)</h3>
<h3><%= t("legal.cookies.s4_2_title") %></h3>
<p>
Se accetti «Tutti i cookie» o abiliti le statistiche nel banner, carichiamo
<strong>Google Analytics 4</strong> per capire come viene usato il sito (pagine visitate, provenienza aggregata, dispositivo).
I dati sono trattati da Google Ireland Limited / Google LLC secondo le loro policy.
<%= raw t("legal.cookies.s4_2_p1_html") %>
</p>
<% if MatchLiveTv.google_analytics_configured? %>
<p class="muted">ID misurazione attivo sul sito: <code><%= MatchLiveTv.google_analytics_measurement_id %></code></p>
<p class="muted"><%= raw t("legal.cookies.s4_2_active_html", id: MatchLiveTv.google_analytics_measurement_id) %></p>
<% else %>
<p class="muted">Google Analytics è configurato solo quando il titolare imposta lID misurazione sul server.</p>
<p class="muted"><%= t("legal.cookies.s4_2_inactive") %></p>
<% end %>
<table class="legal-table">
<thead>
<tr><th>Nome</th><th>Finalità</th><th>Durata</th><th>Fornitore</th></tr>
<tr><th><%= t("legal.cookies.table2_col_name") %></th><th><%= t("legal.cookies.table_col_purpose") %></th><th><%= t("legal.cookies.table_col_duration") %></th><th><%= t("legal.cookies.table_col_provider") %></th></tr>
</thead>
<tbody>
<tr>
<td><code>_ga</code></td>
<td>Distingue gli utenti (statistiche)</td>
<td>2 anni</td>
<td>Google</td>
<td><%= raw t("legal.cookies.s4_2_row1_name_html") %></td>
<td><%= t("legal.cookies.s4_2_row1_purpose") %></td>
<td><%= t("legal.cookies.s4_2_row1_duration") %></td>
<td><%= t("legal.cookies.s4_2_row1_provider") %></td>
</tr>
<tr>
<td><code>_ga_*</code></td>
<td>Mantiene lo stato della sessione Analytics</td>
<td>2 anni</td>
<td>Google</td>
<td><%= raw t("legal.cookies.s4_2_row2_name_html") %></td>
<td><%= t("legal.cookies.s4_2_row2_purpose") %></td>
<td><%= t("legal.cookies.s4_2_row2_duration") %></td>
<td><%= t("legal.cookies.s4_2_row2_provider") %></td>
</tr>
<tr>
<td><code>_gid</code></td>
<td>Distingue gli utenti (statistiche)</td>
<td>24 ore</td>
<td>Google</td>
<td><%= raw t("legal.cookies.s4_2_row3_name_html") %></td>
<td><%= t("legal.cookies.s4_2_row3_purpose") %></td>
<td><%= t("legal.cookies.s4_2_row3_duration") %></td>
<td><%= t("legal.cookies.s4_2_row3_provider") %></td>
</tr>
</tbody>
</table>
<p>
Puoi revocare il consenso dal banner o dalle impostazioni del browser.
Informazioni Google:
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy Google</a>,
<a href="https://tools.google.com/dlpage/gaoptout" target="_blank" rel="noopener noreferrer">componente opt-out Analytics</a>.
<%= raw t(
"legal.cookies.s4_2_p2_html",
google_privacy_link: link_to(t("legal.cookies.s4_2_google_privacy_link_text"), "https://policies.google.com/privacy", target: "_blank", rel: "noopener noreferrer"),
google_optout_link: link_to(t("legal.cookies.s4_2_google_optout_link_text"), "https://tools.google.com/dlpage/gaoptout", target: "_blank", rel: "noopener noreferrer")
) %>
</p>
<h3>4.3 Cookie di terze parti durante il pagamento</h3>
<h3><%= t("legal.cookies.s4_3_title") %></h3>
<p>
Se acquisti un piano Premium, vieni reindirizzato a <strong>Stripe</strong> (checkout sicuro).
Stripe può impostare cookie propri sul dominio <code>stripe.com</code> per prevenire frodi e completare il pagamento.
Non controlliamo direttamente tali cookie: consulta la
<a href="https://stripe.com/privacy" target="_blank" rel="noopener noreferrer">privacy policy di Stripe</a>.
<%= raw t(
"legal.cookies.s4_3_body_html",
stripe_link: link_to(t("legal.cookies.s4_3_stripe_link_text"), "https://stripe.com/privacy", target: "_blank", rel: "noopener noreferrer")
) %>
</p>
<h3>4.4 YouTube e servizi collegati</h3>
<h3><%= t("legal.cookies.s4_4_title") %></h3>
<p>
Lintegrazione YouTube per le dirette avviene tramite API lato server; il sito pubblico non incorpora
player YouTube con cookie di profilazione. I visitatori che aprono un link YouTube esterno
sono soggetti alle policy di Google/YouTube su quel dominio.
<%= t("legal.cookies.s4_4_body") %>
</p>
</section>
<section>
<h2>5. Come disabilitare i cookie dal browser</h2>
<h2><%= t("legal.cookies.s5_title") %></h2>
<p>
Puoi bloccare o cancellare i cookie dalle impostazioni del browser (Chrome, Firefox, Safari, Edge).
Disabilitando i cookie necessari alcune funzioni (es. login) potrebbero non funzionare.
<%= t("legal.cookies.s5_body") %>
</p>
</section>
<section>
<h2>6. Diritti e contatti</h2>
<h2><%= t("legal.cookies.s6_title") %></h2>
<p>
Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso)
scrivi a <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.
Dettagli nel <%= link_to "documento privacy", public_privacy_path %>.
<%= raw t(
"legal.cookies.s6_body_html",
email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}"),
privacy_doc_link: link_to(t("legal.cookies.s6_privacy_doc_link_text"), public_privacy_path)
) %>
</p>
</section>
<p style="margin-top:24px">
<button type="button" class="btn btn-secondary" data-cookie-manage>Gestisci preferenze cookie</button>
<button type="button" class="btn btn-secondary" data-cookie-manage><%= t("legal.cookies.manage_button") %></button>
</p>
</div>
+28 -32
View File
@@ -1,83 +1,79 @@
<% content_for :title, "FAQ — Diretta live partite giovanili | Match Live TV" %>
<% content_for :meta_description, "Domande frequenti su streaming partite giovanili: come guardare la diretta senza app, costi, archivio, pallavolo Under 16, inviti staff e piani Free e Premium." %>
<% content_for :title, t("pages.faq.meta_title") %>
<% content_for :meta_description, t("pages.faq.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_faq_path) %>
<div class="wrap seo-page">
<h1 style="margin-top:24px">Domande frequenti sullo streaming delle partite giovanili</h1>
<h1 style="margin-top:24px"><%= t("pages.faq.title") %></h1>
<p class="seo-lead">
Risposte per genitori, allenatori e dirigenti che vogliono mandare in diretta live le partite
di pallavolo, calcio e altri sport dilettantistici.
<%= t("pages.faq.lead") %>
</p>
<div class="faq-list">
<details class="faq-item" open>
<summary>Come guardo la partita di mio figlio senza installare app?</summary>
<summary><%= t("pages.faq.q1_question") %></summary>
<p>
La società condivide un link (via WhatsApp, email o gruppo squadra). Apri il link dal telefono o dal computer:
vedi la <strong>diretta live</strong> nel browser, senza registrarti. Funziona per nonni, parenti e amici lontani.
<%= raw t("pages.faq.q1_answer_html") %>
</p>
</details>
<details class="faq-item">
<summary>Chi può avviare la diretta dalla palestra?</summary>
<summary><%= t("pages.faq.q2_question") %></summary>
<p>
Coach, dirigenti o volontari invitati dalla società. Usano lapp Match Live TV sul telefono per filmare e,
se serve, un secondo telefono per aggiornare il punteggio. La diretta parte solo quando premo avvio in palestra.
<%= t("pages.faq.q2_answer") %>
</p>
</details>
<details class="faq-item">
<summary>È adatto alla pallavolo giovanile (Under 14, Under 16)?</summary>
<summary><%= t("pages.faq.q3_question") %></summary>
<p>
Sì. Il punteggio a set è pensato per la pallavolo; puoi personalizzare regole per tornei particolari.
Scopri la pagina dedicata: <%= link_to "Match Live TV per pallavolo giovanile", public_pallavolo_path %>.
<%= raw t(
"pages.faq.q3_answer_html",
volleyball_link: link_to(t("pages.faq.q3_volleyball_link"), public_pallavolo_path)
) %>
</p>
</details>
<details class="faq-item">
<summary>Cosa succede se perdo la diretta?</summary>
<summary><%= t("pages.faq.q4_question") %></summary>
<p>
Con i piani Premium Light o Full la partita viene salvata in archivio (30 o 90 giorni) e puoi rivederla dal sito.
Il piano Free non include replay su server, ma la diretta resta gratuita per chi trasmette e per chi guarda.
<%= t("pages.faq.q4_answer") %>
</p>
</details>
<details class="faq-item">
<summary>Quanto costa per la società?</summary>
<summary><%= t("pages.faq.q5_question") %></summary>
<p>
Puoi iniziare con il piano <strong>Free</strong> (limiti su staff e una diretta alla volta).
Premium Light e Full aggiungono più partite in parallelo, archivio e YouTube.
<%= link_to "Confronta i prezzi", public_prezzi_path %>.
<%= raw t(
"pages.faq.q5_answer_html",
pricing_link: link_to(t("pages.faq.q5_pricing_link"), public_prezzi_path)
) %>
</p>
</details>
<details class="faq-item">
<summary>Posso programmare una partita prima del giorno gara?</summary>
<summary><%= t("pages.faq.q6_question") %></summary>
<p>
Sì, dallapp si imposta data e ora: la partita compare in «Prossime partite» sul sito.
Lo streaming parte solo quando lo staff avvia la trasmissione in palestra.
<%= t("pages.faq.q6_answer") %>
</p>
</details>
<details class="faq-item">
<summary>La diretta va anche su YouTube?</summary>
<summary><%= t("pages.faq.q7_question") %></summary>
<p>
Con Premium Light puoi mandarla sul canale Match Live TV; con Premium Full anche sul canale YouTube della società.
Chi preferisce resta sul link Match Live TV, comodo per le famiglie.
<%= t("pages.faq.q7_answer") %>
</p>
</details>
<details class="faq-item">
<summary>Devo aprire porte sul router o avere attrezzatura da TV?</summary>
<summary><%= t("pages.faq.q8_question") %></summary>
<p>
No per chi guarda. Per la società che trasmette basta lo smartphone e una buona connessione in palestra;
lo staff tecnico della piattaforma gestisce linfrastruttura. Niente camere broadcast o mixer.
<%= t("pages.faq.q8_answer") %>
</p>
</details>
</div>
<p style="text-align:center;margin:40px 0">
<%= link_to "Registra la squadra", public_signup_path, class: "btn btn-primary" %>
<%= link_to "Guarda le dirette", public_live_index_path, class: "btn btn-secondary" %>
<%= link_to t("pages.faq.cta_signup"), public_signup_path, class: "btn btn-primary" %>
<%= link_to t("pages.faq.cta_live"), public_live_index_path, class: "btn btn-secondary" %>
</p>
</div>
@@ -1,64 +1,64 @@
<% content_for :title, "Funzionalità streaming partite giovanili — Match Live TV" %>
<% content_for :meta_description, "App per diretta live da telefono: filma la partita, aggiorna il punteggio, condividi il link con genitori e nonni. Archivio gare e piani per società sportive giovanili." %>
<% content_for :title, t("pages.features.meta_title") %>
<% content_for :meta_description, t("pages.features.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_features_path) %>
<div class="wrap">
<h1 style="margin-top:24px">Funzionalità</h1>
<p style="color:#aaa;max-width:640px">Tutto ciò che serve per far seguire le partite a chi non può essere in palestra: semplice per coach e volontari, comodo per genitori, nonni e parenti.</p>
<h1 style="margin-top:24px"><%= t("pages.features.title") %></h1>
<p style="color:#aaa;max-width:640px"><%= t("pages.features.lead") %></p>
<section class="section">
<h2>Per la società e lo staff</h2>
<h2><%= t("pages.features.staff_section_title") %></h2>
<div class="features-grid">
<div class="feature-card">
<h3>Inviti con la propria email</h3>
<p>Il presidente o lowner invita chi filma e chi aggiorna il punteggio: ognuno accede con la sua email, senza condividere password.</p>
<h3><%= t("pages.features.staff_invite_title") %></h3>
<p><%= t("pages.features.staff_invite_body") %></p>
</div>
<div class="feature-card">
<h3>Gestione accessi</h3>
<p>Puoi togliere laccesso a un volontario o annullare un invito ancora in sospeso dalla dashboard della squadra.</p>
<h3><%= t("pages.features.staff_access_title") %></h3>
<p><%= t("pages.features.staff_access_body") %></p>
</div>
<div class="feature-card">
<h3>Piani su misura</h3>
<p>Con Free, Premium Light o Premium Full aumenti quante persone possono aiutare in diretta e quante partite possono andare in parallelo.</p>
<h3><%= t("pages.features.staff_plans_title") %></h3>
<p><%= t("pages.features.staff_plans_body") %></p>
</div>
</div>
</section>
<section class="section">
<h2>In campo e in diretta</h2>
<h2><%= t("pages.features.live_section_title") %></h2>
<div class="features-grid">
<div class="feature-card">
<h3>Filma dal telefono</h3>
<p>Appoggi il telefono e avvii la diretta: anche con il 4G debole la trasmissione resta stabile il più possibile, senza impazzire con cavi o mixer.</p>
<h3><%= t("pages.features.live_phone_title") %></h3>
<p><%= t("pages.features.live_phone_body") %></p>
</div>
<div class="feature-card">
<h3>Punteggio da un secondo telefono</h3>
<p>Un altro volontario aggiorna set e punti da un secondo cellulare: basta inquadrare il codice che compare sullo schermo di chi filma.</p>
<h3><%= t("pages.features.live_score_title") %></h3>
<p><%= t("pages.features.live_score_body") %></p>
</div>
<div class="feature-card">
<h3>Link per famiglie e tifosi</h3>
<p>Condividi un link: genitori, nonni e amici guardano dal browser del telefono o del computer, senza installare app.</p>
<h3><%= t("pages.features.live_link_title") %></h3>
<p><%= t("pages.features.live_link_body") %></p>
</div>
<div class="feature-card">
<h3>Archivio partite</h3>
<p>Con Premium Light o Full le gare restano salvate sul server (30 o 90 giorni) e puoi scaricarle sul telefono quando serve.</p>
<h3><%= t("pages.features.live_archive_title") %></h3>
<p><%= t("pages.features.live_archive_body") %></p>
</div>
<div class="feature-card">
<h3>Anche su YouTube (Match Live TV)</h3>
<p>Con Premium Light la diretta è visibile anche sul canale YouTube Match Live TV, oltre al link sul sito.</p>
<h3><%= t("pages.features.live_youtube_mltv_title") %></h3>
<p><%= t("pages.features.live_youtube_mltv_body") %></p>
</div>
<div class="feature-card">
<h3>Canale YouTube della società</h3>
<p>Con Premium Full puoi mandare la stessa diretta anche sul canale YouTube ufficiale del club.</p>
<h3><%= t("pages.features.live_youtube_club_title") %></h3>
<p><%= t("pages.features.live_youtube_club_body") %></p>
</div>
<div class="feature-card">
<h3>Sul sito della società</h3>
<p>Integrazione per incorporare il player sul sito del club — in arrivo con Premium Full.</p>
<h3><%= t("pages.features.live_website_title") %></h3>
<p><%= t("pages.features.live_website_body") %></p>
</div>
</div>
</section>
<p style="text-align:center;margin:40px 0">
<%= link_to "Registra la squadra", public_signup_path, class: "btn btn-primary" %>
<%= link_to t("pages.features.cta_signup"), public_signup_path, class: "btn btn-primary" %>
</p>
</div>
@@ -1,67 +1,60 @@
<% content_for :title, "Diretta live pallavolo giovanile — Under 14, Under 16 | Match Live TV" %>
<% content_for :meta_description, "Streaming partite di pallavolo giovanile da telefono: diretta live per genitori e nonni, punteggio a set, link da condividere. Ideale per società Under 14, Under 16 e Under 18." %>
<% content_for :title, t("pages.volleyball.meta_title") %>
<% content_for :meta_description, t("pages.volleyball.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_pallavolo_path) %>
<div class="wrap seo-page">
<h1 style="margin-top:24px">Match Live TV per la pallavolo giovanile</h1>
<h1 style="margin-top:24px"><%= t("pages.volleyball.title") %></h1>
<p class="seo-lead">
La soluzione per società, coach e genitori che vogliono una <strong>diretta live</strong> affidabile
delle partite <strong>Under 14, Under 16, Under 18</strong> e settori giovanili — senza diventare telecronisti.
<%= raw t("pages.volleyball.lead_html") %>
</p>
<section class="seo-prose">
<h2>Perché la pallavolo giovanile ha bisogno di uno streaming semplice</h2>
<h2><%= t("pages.volleyball.section1_title") %></h2>
<p>
In palestra i genitori non sono sempre tutti in tribuna: turni di lavoro, fratelli piccoli, trasferte.
Un nonno in unaltra città o un parente allestero vuole comunque vedere il tie-break del terzo set.
Match Live TV nasce per questo: <strong>streaming partite pallavolo giovanile</strong> dal telefono,
con punteggio aggiornato e link immediato da mandare su WhatsApp.
<%= raw t("pages.volleyball.section1_body_html") %>
</p>
<h2>Cosa fa il coach in due minuti</h2>
<h2><%= t("pages.volleyball.section2_title") %></h2>
<ul>
<li>Programma la partita con data, avversario e campo dallapp</li>
<li>Il giorno gara appoggia il telefono e avvia la diretta</li>
<li>Un volontario può aggiornare set e punti da un secondo cellulare</li>
<li>Condivide il link con il gruppo genitori: tutti guardano dal browser</li>
<li><%= t("pages.volleyball.section2_item1") %></li>
<li><%= t("pages.volleyball.section2_item2") %></li>
<li><%= t("pages.volleyball.section2_item3") %></li>
<li><%= t("pages.volleyball.section2_item4") %></li>
</ul>
<h2>Cosa vede la famiglia a casa</h2>
<h2><%= t("pages.volleyball.section3_title") %></h2>
<p>
Nessuna app da installare. Apri il link e segui la partita in diretta live: nome squadre, punteggio,
stato del set. Se hai perso linizio, con Premium la gara resta in archivio per rivederla quando vuoi.
<%= t("pages.volleyball.section3_body") %>
</p>
<h2>Pallavolo: punteggio e regole</h2>
<h2><%= t("pages.volleyball.section4_title") %></h2>
<p>
Set da vincere 2 o 3, punti 25/21/15 configurabili per tornei non standard. Il punteggio si gestisce da un link condiviso (senza account):
in campo resta concentrato sul gioco, non sul telefono.
<%= t("pages.volleyball.section4_body") %>
</p>
<h2>Altri sport giovanili</h2>
<h2><%= t("pages.volleyball.section5_title") %></h2>
<p>
La stessa piattaforma funziona anche per calcio, basket e sport di squadra dilettantistici.
La pallavolo resta lo sport con cui molte società iniziano perché il punteggio a set è già integrato.
<%= t("pages.volleyball.section5_body") %>
</p>
</section>
<div class="features-grid" style="margin-top:32px">
<div class="feature-card">
<h3>Genitori e nonni</h3>
<p>Link su WhatsApp, visione da telefono o TV con browser. Zero registrazione per chi guarda.</p>
<h3><%= t("pages.volleyball.card1_title") %></h3>
<p><%= t("pages.volleyball.card1_body") %></p>
</div>
<div class="feature-card">
<h3>Società e staff</h3>
<p>Inviti con email, più operatori con Premium, revoca accessi dalla dashboard web.</p>
<h3><%= t("pages.volleyball.card2_title") %></h3>
<p><%= t("pages.volleyball.card2_body") %></p>
</div>
<div class="feature-card">
<h3>Archivio gare</h3>
<p>Premium Light/Full: replay per chi ha perso la diretta o vuole rivedere un punto chiave.</p>
<h3><%= t("pages.volleyball.card3_title") %></h3>
<p><%= t("pages.volleyball.card3_body") %></p>
</div>
</div>
<p style="text-align:center;margin:40px 0">
<%= link_to "Inizia gratis", public_signup_path, class: "btn btn-primary" %>
<%= link_to "Domande frequenti", public_faq_path, class: "btn btn-secondary" %>
<%= link_to t("pages.volleyball.cta_start"), public_signup_path, class: "btn btn-primary" %>
<%= link_to t("pages.volleyball.cta_faq"), public_faq_path, class: "btn btn-secondary" %>
</p>
</div>
+20 -16
View File
@@ -1,18 +1,22 @@
<% content_for :title, "Prezzi streaming partite giovanili — Match Live TV" %>
<% content_for :meta_description, "Piani Free, Premium Light e Premium Full per dirette live e archivio partite. Abbonamento annuale per società: più staff, più partite in parallelo, replay e YouTube." %>
<% content_for :title, t("pages.pricing.meta_title") %>
<% content_for :meta_description, t("pages.pricing.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_prezzi_path) %>
<div class="wrap" style="padding-top:24px">
<% if @club %>
<h1>Piani per la tua società</h1>
<h1><%= t("pages.pricing.club_title") %></h1>
<% if @entitlements %>
<%= render "shared/club_subscription_status", club: @club, entitlements: @entitlements, subscription: @subscription %>
<% else %>
<p style="color:#aaa">Società: <strong><%= @club.name %></strong> · <%= link_to "Gestisci abbonamento", public_club_billing_path(@club) %></p>
<p style="color:#aaa"><%= raw t(
"pages.pricing.club_info_html",
name: @club.name,
billing_link: link_to(t("pages.pricing.manage_subscription"), public_club_billing_path(@club))
) %></p>
<% end %>
<% else %>
<h1>Piani per la tua squadra</h1>
<p style="color:#aaa">Abbonamento annuale per società: i responsabili trasmissione avviano la diretta dallapp; il punteggio si gestisce con un link condivisibile.</p>
<h1><%= t("pages.pricing.team_title") %></h1>
<p style="color:#aaa"><%= t("pages.pricing.team_lead") %></p>
<% end %>
<%= render "shared/stripe_secure_payment" %>
@@ -23,19 +27,19 @@
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr>
</thead>
<tbody>
<tr><td>Responsabili trasmissione / anno</td><td>1</td><td>5</td><td>Illimitato</td></tr>
<tr><td>Partite in contemporanea</td><td>1</td><td>3</td><td>10</td></tr>
<tr><td>Live su Match Live TV</td><td></td><td></td><td></td></tr>
<tr><td>YouTube</td><td>No</td><td>Match Live TV</td><td>Canale società</td></tr>
<tr><td>Replay server</td><td>No</td><td>30 gg</td><td>90 gg</td></tr>
<tr><td>Download telefono</td><td>No</td><td></td><td></td></tr>
<tr><td>Prezzo</td><td>€0</td><td>€40/anno o €5/mese</td><td>€200/anno o €20/mese</td></tr>
<tr><td><%= t("pages.pricing.table_staff") %></td><td>1</td><td>5</td><td><%= t("pages.pricing.table_unlimited") %></td></tr>
<tr><td><%= t("pages.pricing.table_matches") %></td><td>1</td><td>3</td><td>10</td></tr>
<tr><td><%= t("pages.pricing.table_live_mltv") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
<tr><td><%= t("pages.pricing.table_youtube") %></td><td><%= t("pages.pricing.table_no") %></td><td>Match Live TV</td><td><%= t("pages.pricing.table_youtube_club") %></td></tr>
<tr><td><%= t("pages.pricing.table_replay") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.plans.replay_days", count: 30) %></td><td><%= t("pages.plans.replay_days", count: 90) %></td></tr>
<tr><td><%= t("pages.pricing.table_download") %></td><td><%= t("pages.pricing.table_no") %></td><td><%= t("pages.pricing.table_yes") %></td><td><%= t("pages.pricing.table_yes") %></td></tr>
<tr><td><%= t("pages.pricing.table_price") %></td><td><%= t("pages.pricing.table_price_free") %></td><td><%= raw t("pages.pricing.table_price_light_html") %></td><td><%= raw t("pages.pricing.table_price_full_html") %></td></tr>
</tbody>
</table>
<div class="card" style="margin-top:32px;text-align:center">
<h3 style="margin-top:0">Esigenze diverse?</h3>
<p style="color:#aaa;margin-bottom:16px">Per società con più squadre, tornei o integrazioni custom, contattaci: troviamo insieme l'offerta migliore.</p>
<%= mail_to "info@matchlive.it", "Contattaci", class: "btn btn-primary" %>
<h3 style="margin-top:0"><%= t("pages.pricing.different_title") %></h3>
<p style="color:#aaa;margin-bottom:16px"><%= t("pages.pricing.different_body") %></p>
<%= mail_to "info@matchlive.it", t("pages.pricing.contact_button"), class: "btn btn-primary" %>
</div>
</div>
+109 -101
View File
@@ -1,192 +1,200 @@
<% content_for :title, "Informativa privacy — Match Live TV" %>
<% content_for :meta_description, "Informativa privacy GDPR di Match Live TV: dati account, streaming partite giovanili, immagini di minori, pagamenti Stripe e diritti degli interessati." %>
<% content_for :title, t("legal.privacy.title") %>
<% content_for :meta_description, t("legal.privacy.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_privacy_path) %>
<div class="wrap legal-doc">
<h1>Informativa sul trattamento dei dati personali</h1>
<p class="legal-meta">Ultimo aggiornamento: <%= legal_last_updated %> · Ai sensi del Regolamento (UE) 2016/679 (GDPR)</p>
<h1><%= t("legal.privacy.h1") %></h1>
<p class="legal-meta"><%= raw t("legal.privacy.meta_html", date: legal_last_updated) %></p>
<section>
<h2>1. Titolare del trattamento</h2>
<h2><%= t("legal.privacy.s1_title") %></h2>
<p>
Il titolare del trattamento dei dati personali raccolti tramite il sito e i servizi Match Live TV è:
<%= t("legal.privacy.s1_intro") %>
</p>
<ul>
<li><strong><%= MatchLiveTv.privacy_controller_name %></strong></li>
<li>Indirizzo: <%= MatchLiveTv.privacy_controller_address %></li>
<li>Email per privacy e diritti: <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a></li>
<li><%= t("legal.privacy.s1_label_address") %> <%= MatchLiveTv.privacy_controller_address %></li>
<li><%= t("legal.privacy.s1_label_email") %> <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a></li>
<% if MatchLiveTv.privacy_controller_vat.present? %>
<li>Partita IVA / CF: <%= MatchLiveTv.privacy_controller_vat %></li>
<li><%= t("legal.privacy.s1_label_vat") %> <%= MatchLiveTv.privacy_controller_vat %></li>
<% end %>
</ul>
<p>Per richieste relative a questa informativa o per esercitare i diritti previsti dal GDPR, scrivi allindirizzo email indicato.</p>
<p><%= t("legal.privacy.s1_outro") %></p>
</section>
<section>
<h2>2. Ambito di applicazione</h2>
<p>La presente informativa si applica a:</p>
<h2><%= t("legal.privacy.s2_title") %></h2>
<p><%= t("legal.privacy.s2_intro") %></p>
<ul>
<li>visitatori del sito web Match Live TV (pagine informative, elenco dirette, registrazione squadra);</li>
<li>utenti registrati (referenti di società sportive, coach, staff invitato);</li>
<li>spettatori che accedono alle dirette tramite link (in genere senza creare un account);</li>
<li>utilizzo dellapp mobile collegata al servizio.</li>
<li><%= t("legal.privacy.s2_item1") %></li>
<li><%= t("legal.privacy.s2_item2") %></li>
<li><%= t("legal.privacy.s2_item3") %></li>
<li><%= t("legal.privacy.s2_item4") %></li>
</ul>
</section>
<section>
<h2>3. Tipologie di dati trattati</h2>
<p>Possiamo trattare, a seconda delluso del servizio:</p>
<h2><%= t("legal.privacy.s3_title") %></h2>
<p><%= t("legal.privacy.s3_intro") %></p>
<ul>
<li><strong>Dati identificativi e di contatto:</strong> nome, indirizzo email, credenziali di accesso (password conservata in forma crittografata).</li>
<li><strong>Dati della squadra:</strong> nome società, sport, partite programmate, avversario, luogo, orari.</li>
<li><strong>Dati di streaming e tecnici:</strong> stato sessione, punteggio, telemetria di rete/bitrate, log di connessione al servizio video.</li>
<li><strong>Contenuti audiovisivi:</strong> flussi video e audio delle partite trasmesse in diretta e, con i piani Premium, registrazioni (replay) conservate per un periodo limitato.</li>
<li><strong>Dati di pagamento:</strong> per i piani a pagamento, Stripe tratta i dati necessari al pagamento (es. metodo di pagamento, intestazione fattura). Il titolare non conserva i numeri completi della carta.</li>
<li><strong>Dati di navigazione:</strong> log del server, indirizzo IP, user agent, necessari per sicurezza e funzionamento del sito.</li>
<li><%= raw t("legal.privacy.s3_item1_html") %></li>
<li><%= raw t("legal.privacy.s3_item2_html") %></li>
<li><%= raw t("legal.privacy.s3_item3_html") %></li>
<li><%= raw t("legal.privacy.s3_item4_html") %></li>
<li><%= raw t("legal.privacy.s3_item5_html") %></li>
<li><%= raw t("legal.privacy.s3_item6_html") %></li>
</ul>
</section>
<section>
<h2>4. Trattamento di immagini e dati di atleti minorenni</h2>
<h2><%= t("legal.privacy.s4_title") %></h2>
<p>
Match Live TV è destinato anche a competizioni e categorie giovanili (es. Under 14, Under 16, Under 18).
Le dirette possono riprendere <strong>atleti minorenni</strong> identificabili (immagine, voce, numero di maglia, dati di gioco).
<%= raw t("legal.privacy.s4_p1_html") %>
</p>
<p><strong>Chi è responsabile del trattamento delle immagini dei minori</strong></p>
<p><strong><%= t("legal.privacy.s4_lead1") %></strong></p>
<p>
La <strong>società sportiva</strong> che avvia la trasmissione, in qualità di organizzatrice dellevento sportivo,
è in primo luogo responsabile di assicurare una base giuridica valida (tipicamente <strong>consenso dei genitori/tutori</strong>,
oppure altro titolo previsto dalla legge e dal regolamento della federazione di riferimento) prima di filmare e diffondere le immagini.
<%= raw t("legal.privacy.s4_p2_html") %>
</p>
<p><strong>Ruolo di Match Live TV</strong></p>
<p><strong><%= t("legal.privacy.s4_lead2") %></strong></p>
<p>
Il titolare del servizio fornisce la piattaforma tecnica e, per le trasmissioni effettuate dagli utenti autorizzati,
può agire come <strong>responsabile del trattamento</strong> su istruzioni della società o, in alcuni casi, come titolare
per i trattamenti strettamente necessari allerogazione del servizio (hosting, sicurezza, fatturazione).
<%= raw t("legal.privacy.s4_p3_html") %>
</p>
<p><strong>Misure richieste alla società sportiva</strong></p>
<p><strong><%= t("legal.privacy.s4_lead3") %></strong></p>
<ul>
<li>informare genitori/tutori sulluso di video in diretta e in archivio;</li>
<li>raccogliere e documentare i consensi necessari prima della diretta;</li>
<li>limitare la diffusione del link a persone autorizzate (familiari, staff);</li>
<li>non pubblicare su canali aperti se non previsto dal consenso o dal regolamento sportivo;</li>
<li>richiedere rimozione di registrazioni quando ne ha diritto linteressato o il tutore.</li>
<li><%= t("legal.privacy.s4_item1") %></li>
<li><%= t("legal.privacy.s4_item2") %></li>
<li><%= t("legal.privacy.s4_item3") %></li>
<li><%= t("legal.privacy.s4_item4") %></li>
<li><%= t("legal.privacy.s4_item5") %></li>
</ul>
<p>
Le richieste dei genitori/tutori relative a immagini di minori possono essere inoltrate alla società sportiva
e, per supporto tecnico o cancellazione da archivio, anche a
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.
<%= raw t(
"legal.privacy.s4_p4_html",
email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}")
) %>
</p>
</section>
<section>
<h2>5. Finalità e basi giuridiche</h2>
<h2><%= t("legal.privacy.s5_title") %></h2>
<table class="legal-table">
<thead>
<tr><th>Finalità</th><th>Base giuridica (art. 6 GDPR)</th></tr>
<tr><th><%= t("legal.privacy.s5_col_purpose") %></th><th><%= t("legal.privacy.s5_col_basis") %></th></tr>
</thead>
<tbody>
<tr>
<td>Registrazione account, gestione squadra e staff</td>
<td>Esecuzione del contratto / misure precontrattuali</td>
<td><%= t("legal.privacy.s5_row1_purpose") %></td>
<td><%= t("legal.privacy.s5_row1_basis") %></td>
</tr>
<tr>
<td>Erogazione dirette, punteggio live, archivio (piani Premium)</td>
<td>Esecuzione del contratto; legittimo interesse alla sicurezza del servizio</td>
<td><%= t("legal.privacy.s5_row2_purpose") %></td>
<td><%= t("legal.privacy.s5_row2_basis") %></td>
</tr>
<tr>
<td>Pagamenti e fatturazione (Stripe)</td>
<td>Esecuzione del contratto; obblighi di legge</td>
<td><%= t("legal.privacy.s5_row3_purpose") %></td>
<td><%= t("legal.privacy.s5_row3_basis") %></td>
</tr>
<tr>
<td>Assistenza, sicurezza, prevenzione abusi</td>
<td>Legittimo interesse; obblighi di legge</td>
<td><%= t("legal.privacy.s5_row4_purpose") %></td>
<td><%= t("legal.privacy.s5_row4_basis") %></td>
</tr>
<tr>
<td>Comunicazioni di servizio (es. reset password)</td>
<td>Esecuzione del contratto</td>
<td><%= t("legal.privacy.s5_row5_purpose") %></td>
<td><%= t("legal.privacy.s5_row5_basis") %></td>
</tr>
<tr>
<td>Marketing diretto (solo se previsto e con consenso ove richiesto)</td>
<td>Consenso o legittimo interesse, secondo i casi</td>
<td><%= t("legal.privacy.s5_row6_purpose") %></td>
<td><%= t("legal.privacy.s5_row6_basis") %></td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>6. Destinatari e responsabili del trattamento</h2>
<p>I dati possono essere comunicati a fornitori che agiscono come responsabili del trattamento, tra cui:</p>
<h2><%= t("legal.privacy.s6_title") %></h2>
<p><%= t("legal.privacy.s6_intro") %></p>
<ul>
<li>fornitore di hosting e infrastruttura cloud/server;</li>
<li><strong>Stripe</strong> (pagamenti) — <a href="https://stripe.com/it/privacy" rel="noopener" target="_blank">privacy Stripe</a>;</li>
<li>fornitore streaming video (MediaMTX / infrastruttura RTMP-HLS);</li>
<li>eventuale integrazione <strong>YouTube</strong> se attivata dalla società (soggetto a policy Google);</li>
<li>fornitore email transazionale (invio link reset password, inviti).</li>
<li><%= t("legal.privacy.s6_item1") %></li>
<li>
<%= raw t(
"legal.privacy.s6_item2_html",
stripe_link: link_to(t("legal.privacy.s6_item2_link_text"), "https://stripe.com/it/privacy", rel: "noopener", target: "_blank")
) %>
</li>
<li><%= t("legal.privacy.s6_item3") %></li>
<li><%= raw t("legal.privacy.s6_item4_html") %></li>
<li><%= t("legal.privacy.s6_item5") %></li>
</ul>
<p>I dati non sono venduti a terzi per finalità di profilazione commerciale.</p>
<p><%= t("legal.privacy.s6_outro") %></p>
</section>
<section>
<h2>7. Trasferimenti extra SEE</h2>
<h2><%= t("legal.privacy.s7_title") %></h2>
<p>
Alcuni fornitori (es. Stripe, Google/YouTube) possono trattare dati anche fuori dallo Spazio Economico Europeo.
In tal caso si applicano garanzie adeguate previste dal GDPR (clausole contrattuali standard, decisioni di adeguatezza).
<%= t("legal.privacy.s7_body") %>
</p>
</section>
<section>
<h2>8. Conservazione</h2>
<h2><%= t("legal.privacy.s8_title") %></h2>
<ul>
<li><strong>Account utente:</strong> per tutta la durata del rapporto contrattuale e, dopo la cessazione, per il tempo necessario a obblighi di legge o contestazioni (in genere fino a 10 anni per documenti contabili ove applicabile).</li>
<li><strong>Registrazioni partite (replay):</strong> secondo il piano (es. 30 o 90 giorni), poi cancellazione automatica o su richiesta.</li>
<li><strong>Log tecnici:</strong> periodo limitato (es. 3090 giorni) salvo obblighi di sicurezza.</li>
<li><strong>Token reset password:</strong> validità massima <%= MatchLiveTv.password_reset_expiry_hours %> ore.</li>
<li><%= raw t("legal.privacy.s8_item1_html") %></li>
<li><%= raw t("legal.privacy.s8_item2_html") %></li>
<li><%= raw t("legal.privacy.s8_item3_html") %></li>
<li><%= raw t("legal.privacy.s8_item4_html", hours: MatchLiveTv.password_reset_expiry_hours) %></li>
</ul>
</section>
<section>
<h2>9. Diritti degli interessati</h2>
<p>In qualità di interessato hai diritto di:</p>
<h2><%= t("legal.privacy.s9_title") %></h2>
<p><%= t("legal.privacy.s9_intro") %></p>
<ul>
<li>accedere ai tuoi dati e ottenerne copia;</li>
<li>rettificarli se inesatti;</li>
<li>chiederne la cancellazione (nei limiti di legge);</li>
<li>limitare o opporti al trattamento, ove applicabile;</li>
<li>revocare il consenso senza pregiudicare la liceità del trattamento basato sul consenso prestato prima della revoca;</li>
<li>proporre reclamo allAutorità Garante per la protezione dei dati personali (<a href="https://www.garanteprivacy.it" rel="noopener" target="_blank">garanteprivacy.it</a>).</li>
<li><%= t("legal.privacy.s9_item1") %></li>
<li><%= t("legal.privacy.s9_item2") %></li>
<li><%= t("legal.privacy.s9_item3") %></li>
<li><%= t("legal.privacy.s9_item4") %></li>
<li><%= t("legal.privacy.s9_item5") %></li>
<li>
<%= raw t(
"legal.privacy.s9_item6_html",
garante_link: link_to("garanteprivacy.it", "https://www.garanteprivacy.it", rel: "noopener", target: "_blank")
) %>
</li>
</ul>
<p>Per esercitare i diritti: <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.</p>
</section>
<section>
<h2>10. Sicurezza</h2>
<p>
Adottiamo misure tecniche e organizzative adeguate (accesso autenticato, password crittografate,
comunicazioni HTTPS, segregazione ambienti, limitazione accessi staff). Nessun sistema è invulnerabile:
se sospetti un accesso non autorizzato, cambia password e contattaci.
<%= raw t(
"legal.privacy.s9_outro_html",
email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}")
) %>
</p>
</section>
<section>
<h2>11. Cookie e tecnologie simili</h2>
<h2><%= t("legal.privacy.s10_title") %></h2>
<p>
Il sito utilizza cookie tecnici necessari (sessione di login, sicurezza, memorizzazione delle preferenze cookie)
e, previo consenso tramite il banner, <strong>Google Analytics</strong> per statistiche aggregate.
</p>
<p>
Puoi gestire le preferenze in qualsiasi momento dal link «Gestisci cookie» nel footer
o consultare la <%= link_to "Cookie policy", public_cookies_path %>.
<%= t("legal.privacy.s10_body") %>
</p>
</section>
<section>
<h2>12. Modifiche</h2>
<h2><%= t("legal.privacy.s11_title") %></h2>
<p>
Il titolare può aggiornare questa informativa. La data in alto indica lultima revisione.
In caso di modifiche rilevanti, sarà fornita comunicazione adeguata (es. avviso sul sito o email agli account registrati).
<%= raw t("legal.privacy.s11_p1_html") %>
</p>
<p>
<%= raw t(
"legal.privacy.s11_p2_html",
cookie_policy_link: link_to(t("legal.privacy.s11_cookie_policy_link_text"), public_cookies_path)
) %>
</p>
</section>
<p class="legal-back"><%= link_to "← Torna alla home", root_path %></p>
<section>
<h2><%= t("legal.privacy.s12_title") %></h2>
<p>
<%= t("legal.privacy.s12_body") %>
</p>
</section>
<p class="legal-back"><%= link_to t("legal.common.back_home"), root_path %></p>
</div>
+60 -66
View File
@@ -1,134 +1,128 @@
<% content_for :title, "Termini di servizio — Match Live TV" %>
<% content_for :meta_description, "Termini e condizioni d'uso di Match Live TV per società sportive: dirette live, minori, piani, pagamenti Stripe e responsabilità degli utenti." %>
<% content_for :title, t("legal.terms.title") %>
<% content_for :meta_description, t("legal.terms.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_termini_path) %>
<div class="wrap legal-doc">
<h1>Termini e condizioni di servizio</h1>
<p class="legal-meta">Ultimo aggiornamento: <%= legal_last_updated %></p>
<h1><%= t("legal.terms.h1") %></h1>
<p class="legal-meta"><%= t("legal.terms.meta", date: legal_last_updated) %></p>
<section>
<h2>1. Oggetto</h2>
<h2><%= t("legal.terms.s1_title") %></h2>
<p>
I presenti Termini regolano luso della piattaforma <strong>Match Live TV</strong> (sito web, applicazione mobile
e servizi connessi) per la gestione di dirette sportive dilettantistiche/giovanili, punteggio live,
condivisione link agli spettatori e funzioni Premium (archivio, YouTube, ecc.).
<%= raw t("legal.terms.s1_p1_html") %>
</p>
<p>
Il servizio è erogato da <strong><%= MatchLiveTv.privacy_controller_name %></strong>
(di seguito «Fornitore»). Registrandoti o utilizzando il servizio accetti integralmente questi Termini
e l<%= link_to "informativa privacy", public_privacy_path %>.
<%= raw t(
"legal.terms.s1_p2_html",
provider_name: MatchLiveTv.privacy_controller_name,
privacy_link: link_to(t("legal.terms.s1_privacy_link_text"), public_privacy_path)
) %>
</p>
</section>
<section>
<h2>2. Chi può usare il servizio</h2>
<h2><%= t("legal.terms.s2_title") %></h2>
<ul>
<li>Il referente che registra la squadra dichiara di avere almeno <strong>18 anni</strong> e poteri per vincolare la società sportiva.</li>
<li>I responsabili trasmissione invitati usano credenziali personali e si impegnano a custodirle.</li>
<li>Gli spettatori che aprono il link della diretta di solito non necessitano di account.</li>
<li><%= raw t("legal.terms.s2_item1_html") %></li>
<li><%= t("legal.terms.s2_item2") %></li>
<li><%= t("legal.terms.s2_item3") %></li>
</ul>
</section>
<section>
<h2>3. Account, piani e pagamenti</h2>
<h2><%= t("legal.terms.s3_title") %></h2>
<p>
Sono disponibili piani Free e Premium (Light / Full) con limiti su staff, dirette concorrenti, archivio e integrazioni.
I prezzi sono indicati nella pagina <%= link_to "Prezzi", public_prezzi_path %>.
<%= raw t(
"legal.terms.s3_p1_html",
pricing_link: link_to(t("legal.terms.s3_pricing_link_text"), public_prezzi_path)
) %>
</p>
<p>
I pagamenti dei piani a pagamento sono gestiti da <strong>Stripe</strong>. Accettando il checkout,
accetti anche i termini di Stripe per il pagamento. Il Fornitore non memorizza i dati completi della carta.
<%= raw t("legal.terms.s3_p2_html") %>
</p>
<p>Rinnovi, disdetta e diritto di recesso seguono quanto comunicato al momento dellacquisto e la normativa applicabile.</p>
<p><%= t("legal.terms.s3_p3") %></p>
</section>
<section>
<h2>4. Dirette, contenuti e protezione dei minori</h2>
<p><strong>Responsabilità della società sportiva</strong></p>
<h2><%= t("legal.terms.s4_title") %></h2>
<p><strong><%= t("legal.terms.s4_lead1") %></strong></p>
<p>
La società che avvia una diretta è responsabile dei contenuti trasmessi (immagini, audio, commenti)
e della conformità alle leggi, al regolamento federale e ai consensi necessari, in particolare quando
sono ripresi <strong>atleti minorenni</strong>.
<%= raw t("legal.terms.s4_p1_html") %>
</p>
<p>La società si impegna a:</p>
<p><%= t("legal.terms.s4_p2") %></p>
<ul>
<li>ottenere e conservare i consensi genitoriali/tutori o altra base giuridica prima della ripresa;</li>
<li>informare genitori e atleti su dove e per quanto tempo è visibile la diretta/archivio;</li>
<li>non trasmettere contenuti illegali, diffamatori, violenti o non pertinenti allevento sportivo;</li>
<li>rispettare il diritto allimmagine e alla voce di tutte le persone inquadrate.</li>
<li><%= t("legal.terms.s4_item1") %></li>
<li><%= t("legal.terms.s4_item2") %></li>
<li><%= t("legal.terms.s4_item3") %></li>
<li><%= t("legal.terms.s4_item4") %></li>
</ul>
<p>
Il Fornitore può sospendere o interrompere trasmissioni e account in caso di segnalazioni fondate
o violazioni gravi, senza pregiudizio di azioni legali.
<%= t("legal.terms.s4_p3") %>
</p>
</section>
<section>
<h2>5. Uso consentito e divieti</h2>
<p>È vietato:</p>
<h2><%= t("legal.terms.s5_title") %></h2>
<p><%= t("legal.terms.s5_intro") %></p>
<ul>
<li>usare il servizio per finalità non sportive o commerciali non autorizzate;</li>
<li>aggirare limiti tecnici o di piano (staff, dirette concorrenti);</li>
<li>condividere credenziali o pubblicare link su canali che espongono minori a rischi non previsti dal consenso;</li>
<li>caricare malware, effettuare attacchi o interferire con linfrastruttura;</li>
<li>rivendere il servizio senza accordo scritto con il Fornitore.</li>
<li><%= t("legal.terms.s5_item1") %></li>
<li><%= t("legal.terms.s5_item2") %></li>
<li><%= t("legal.terms.s5_item3") %></li>
<li><%= t("legal.terms.s5_item4") %></li>
<li><%= t("legal.terms.s5_item5") %></li>
</ul>
</section>
<section>
<h2>6. Proprietà intellettuale</h2>
<h2><%= t("legal.terms.s6_title") %></h2>
<p>
Marchi, software, design e documentazione del servizio restano del Fornitore o dei suoi licenzianti.
La società sportiva conserva i diritti sui propri contenuti video; concede al Fornitore una licenza
limitata a ospitarli, elaborarli e distribuirli tramite la piattaforma per erogare il servizio.
<%= t("legal.terms.s6_body") %>
</p>
</section>
<section>
<h2>7. Disponibilità e limitazioni</h2>
<h2><%= t("legal.terms.s7_title") %></h2>
<p>
Il servizio dipende da reti mobili, hardware e fornitori terzi. Non garantiamo assenza assoluta di interruzioni,
ritardi o perdita di dati, pur adottando misure ragionevoli. La qualità video può variare in base alla connessione in palestra.
<%= t("legal.terms.s7_body") %>
</p>
</section>
<section>
<h2>8. Limitazione di responsabilità</h2>
<h2><%= t("legal.terms.s8_title") %></h2>
<p>
Nei limiti consentiti dalla legge italiana, il Fornitore non risponde di danni indiretti, perdita di profitto,
o contenuti trasmessi dagli utenti. La responsabilità complessiva verso un cliente professionale non eccede,
ove possibile, limporto pagato negli ultimi 12 mesi per il piano sottoscritto.
<%= t("legal.terms.s8_p1") %>
</p>
<p>Nulla esclude responsabilità per dolo o colpa grave, né diritti inderogabili del consumatore ove applicabili.</p>
<p><%= t("legal.terms.s8_p2") %></p>
</section>
<section>
<h2>9. Recesso, sospensione e cancellazione</h2>
<h2><%= t("legal.terms.s9_title") %></h2>
<p>
Puoi richiedere la chiusura dellaccount scrivendo a
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.
Il Fornitore può sospendere laccount per inadempienza, mancato pagamento o violazione dei Termini.
Alla cessazione, i dati sono trattati come indicato in privacy (conservazione limitata o cancellazione).
<%= raw t(
"legal.terms.s9_body_html",
email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}")
) %>
</p>
</section>
<section>
<h2>10. Legge applicabile e foro</h2>
<h2><%= t("legal.terms.s10_title") %></h2>
<p>
I Termini sono regolati dalla <strong>legge italiana</strong>. Per controversie con consumatori si applicano
le norme inderogabili di protezione del consumatore e il foro del consumatore ove previsto.
Per clienti professionali, foro competente in Italia presso la sede del Fornitore, salvo diverso accordo.
<%= raw t("legal.terms.s10_body_html") %>
</p>
</section>
<section>
<h2>11. Contatti</h2>
<h2><%= t("legal.terms.s11_title") %></h2>
<p>
<%= MatchLiveTv.privacy_controller_name %><br>
<%= MatchLiveTv.privacy_controller_address %><br>
Email: <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>
<%= raw t(
"legal.terms.s11_body_html",
provider_name: MatchLiveTv.privacy_controller_name,
provider_address: MatchLiveTv.privacy_controller_address,
email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}")
) %>
</p>
</section>
<p class="legal-back"><%= link_to "← Torna alla home", root_path %> · <%= link_to "Privacy", public_privacy_path %></p>
<p class="legal-back"><%= link_to t("legal.common.back_home"), root_path %> · <%= link_to t("legal.common.privacy_nav"), public_privacy_path %></p>
</div>
@@ -1,26 +1,26 @@
<% content_for :title, "Nuova password — Match Live TV" %>
<% content_for :title, t("auth.password_reset.meta_title") %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>Scegli una nuova password</h1>
<h1><%= t("auth.password_reset.title") %></h1>
<div class="card">
<%= form_with url: public_password_reset_path, method: :patch, local: true do %>
<%= hidden_field_tag :token, @token %>
<%= render "shared/input_toggle",
name: :password,
label: "Nuova password (min. 8 caratteri)",
label: t("auth.password_reset.new_password_label"),
input_type: "text",
required: true,
minlength: 8,
autocomplete: "new-password" %>
<%= render "shared/input_toggle",
name: :password_confirmation,
label: "Conferma password",
label: t("auth.password_confirmation"),
input_type: "text",
required: true,
autocomplete: "new-password" %>
<%= submit_tag "Salva password", class: "btn btn-primary" %>
<%= submit_tag t("auth.password_reset.submit"), class: "btn btn-primary" %>
<% end %>
<p class="auth-footer"><%= link_to "Torna al login", public_login_path %></p>
<p class="auth-footer"><%= link_to t("auth.password_reset.back_to_login"), public_login_path %></p>
</div>
</section>
@@ -1,15 +1,15 @@
<% content_for :title, "Password dimenticata — Match Live TV" %>
<% content_for :title, t("auth.password_forgot.meta_title") %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>Password dimenticata</h1>
<p class="auth-lead">Inserisci lemail dellaccount: ti invieremo un link per reimpostare la password.</p>
<h1><%= t("auth.password_forgot.title") %></h1>
<p class="auth-lead"><%= t("auth.password_forgot.lead") %></p>
<div class="card">
<%= form_with url: public_password_forgot_path, method: :post, local: true do %>
<%= label_tag :email, "Email" %>
<%= label_tag :email, t("auth.email") %>
<%= email_field_tag :email, params[:email], required: true, autocomplete: "email" %>
<%= submit_tag "Invia link di reset", class: "btn btn-primary" %>
<%= submit_tag t("auth.password_forgot.submit"), class: "btn btn-primary" %>
<% end %>
<p class="auth-footer"><%= link_to "Torna al login", public_login_path %></p>
<p class="auth-footer"><%= link_to t("auth.password_forgot.back_to_login"), public_login_path %></p>
</div>
</section>
@@ -22,4 +22,4 @@
<button type="button" class="regia-btn regia-btn--minus" data-action="away_undo" style="width:100%;margin-top:4px"></button>
</div>
</div>
<button type="button" class="regia-btn regia-btn--yellow" data-action="advance_period" style="margin-top:12px">Prossimo quarto</button>
<button type="button" class="regia-btn regia-btn--yellow" data-action="advance_period" style="margin-top:12px"><%= t("regia.board.next_period") %></button>
@@ -1,4 +1,4 @@
<p class="regia-sets" id="sets-line">Punteggio libero</p>
<p class="regia-sets" id="sets-line"><%= t("regia.board.generic_score") %></p>
<p class="regia-score-line" id="score-line"><%= live_score_points_label(@match, @score) %></p>
<p class="regia-partials" id="partials-line" hidden></p>
@@ -18,4 +18,4 @@
<button type="button" class="regia-btn regia-btn--minus" data-action="away_undo" style="width:100%;margin-top:6px"></button>
</div>
</div>
<button type="button" class="regia-btn regia-btn--yellow" data-action="advance_period" style="margin-top:12px">Prossimo tempo</button>
<button type="button" class="regia-btn regia-btn--yellow" data-action="advance_period" style="margin-top:12px"><%= t("regia.board.next_half") %></button>
@@ -1,10 +1,10 @@
<% data = @score.data || {} %>
<p class="regia-sets" id="sets-line">Cronometro</p>
<p class="regia-sets" id="sets-line"><%= t("regia.board.stopwatch") %></p>
<p class="regia-score-line" id="score-line" hidden></p>
<p class="regia-partials" id="partials-line" hidden></p>
<p class="regia-clock-line" id="clock-line"><%= format_clock_secs(data["clock_secs"], count_up: true) %></p>
<div class="regia-clock-actions" style="margin-top:16px">
<button type="button" class="regia-btn regia-btn--outline" data-action="clock_toggle"><%= data["clock_running"] ? "Pausa" : "Avvia" %></button>
<button type="button" class="regia-btn regia-btn--outline" data-action="clock_reset">Reset</button>
<button type="button" class="regia-btn regia-btn--outline" data-action="clock_toggle"><%= data["clock_running"] ? t("regia.board.pause") : t("regia.board.start") %></button>
<button type="button" class="regia-btn regia-btn--outline" data-action="clock_reset"><%= t("regia.board.reset") %></button>
</div>
@@ -1,7 +1,7 @@
<p class="regia-sets" id="sets-line"><%= live_score_sets_label(@score) || "—" %></p>
<p class="regia-score-line" id="score-line"><%= live_score_points_label(@match, @score) %></p>
<p class="regia-partials" id="partials-line"<%= " hidden" unless live_score_partials_label(@score).present? %>>
Parziali: <%= live_score_partials_label(@score) %>
<%= t("regia.board.partials_prefix") %> <%= live_score_partials_label(@score) %>
</p>
<div class="regia-team-row">
@@ -12,7 +12,7 @@
<button type="button" class="regia-btn regia-btn--minus" data-action="home_undo" style="width:100%;margin-top:6px"></button>
</div>
<div class="regia-center-hint" id="center-hint">
<%= Scoring::Rules.from_match(@match).points_target(@score.current_set) %> pt
<%= Scoring::Rules.from_match(@match).points_target(@score.current_set) %> <%= t("regia.board.points_suffix") %>
</div>
<div>
<div class="regia-team-name"><%= @match.opponent_name %></div>
@@ -21,4 +21,4 @@
<button type="button" class="regia-btn regia-btn--minus" data-action="away_undo" style="width:100%;margin-top:6px"></button>
</div>
</div>
<button type="button" class="regia-btn regia-btn--yellow" data-action="close_set" style="margin-top:12px">Chiudi set</button>
<button type="button" class="regia-btn regia-btn--yellow" data-action="close_set" style="margin-top:12px"><%= t("regia.board.close_set") %></button>
+16 -15
View File
@@ -1,4 +1,4 @@
<% content_for :title, "Regia#{@team.name} vs #{@match.opponent_name}" %>
<% content_for :title, t("regia.title", home: @team.name, away: @match.opponent_name) %>
<% content_for :robots, "noindex, nofollow" %>
<% content_for :head do %>
@@ -8,6 +8,7 @@
<% end %>
<div class="regia-page" id="regia-app"
data-i18n="<%= j regia_js_i18n_json %>"
data-token="<%= j params[:token] %>"
data-board="<%= @match.effective_board_type %>"
data-status-url="<%= j public_regia_status_path(params[:token]) %>"
@@ -29,34 +30,34 @@
<header class="regia-header">
<h1><%= @team.name %> vs <%= @match.opponent_name %></h1>
<p id="regia-subtitle">Regia · <%= regia_header_subtitle(@match, @score) %></p>
<p id="regia-subtitle"><%= t("regia.subtitle_prefix") %> <%= regia_header_subtitle(@match, @score) %></p>
<span id="regia-badge" class="regia-badge <%= @stream_closed ? 'regia-badge--ended' : (@session.paused? ? 'regia-badge--wait' : (@on_air ? 'regia-badge--live' : 'regia-badge--wait')) %>">
<%= @stream_closed ? "Terminata" : (@session.paused? ? "In pausa" : (@on_air ? "In onda" : "In attesa")) %>
<%= @stream_closed ? t("regia.status.ended") : (@session.paused? ? t("regia.status.paused") : (@on_air ? t("regia.status.live") : t("regia.status.waiting"))) %>
</span>
</header>
<% if @live_share_url.present? %>
<div class="regia-share-group">
<p class="regia-share-label">Link diretta (spettatori)</p>
<p class="regia-share-label"><%= t("regia.live_share_label") %></p>
<div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-live" style="margin:0">
Condividi diretta
<%= t("regia.share_live") %>
</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-live" style="margin:0">
Copia link
<%= t("regia.copy_link") %>
</button>
</div>
</div>
<% end %>
<div class="regia-share-group">
<p class="regia-share-label">Link regia (punteggio)</p>
<p class="regia-share-label"><%= t("regia.regia_share_label") %></p>
<div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-regia" style="margin:0">
Condividi link regia
<%= t("regia.share_regia") %>
</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-regia" style="margin:0">
Copia link
<%= t("regia.copy_link") %>
</button>
</div>
</div>
@@ -66,7 +67,7 @@
<video id="regia-preview" playsinline muted autoplay></video>
<% end %>
<div id="regia-preview-placeholder" class="regia-preview__placeholder">
<%= @stream_closed ? "Diretta terminata" : "Anteprima in attesa del segnale…" %>
<%= @stream_closed ? t("regia.preview_ended") : t("regia.preview_waiting") %>
</div>
</div>
@@ -75,8 +76,8 @@
</section>
<% unless @stream_closed %>
<button type="button" class="regia-btn regia-btn--outline" id="btn-pause"><%= @session.paused? ? "Riprendi diretta" : "Metti in pausa" %></button>
<button type="button" class="regia-btn regia-btn--danger" id="btn-stop">Chiudi diretta</button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-pause"><%= @session.paused? ? t("regia.resume") : t("regia.pause") %></button>
<button type="button" class="regia-btn regia-btn--danger" id="btn-stop"><%= t("regia.stop") %></button>
<% end %>
</div>
@@ -84,11 +85,11 @@
<div id="regia-modal" class="regia-modal" aria-hidden="true">
<div class="regia-modal__card">
<h2 id="modal-title">Set vinto</h2>
<h2 id="modal-title"><%= t("regia.modal.set_won_title") %></h2>
<p id="modal-body"></p>
<div class="regia-modal__actions">
<button type="button" class="regia-btn regia-btn--yellow" id="modal-confirm">Chiudi set</button>
<button type="button" class="regia-btn regia-btn--outline" id="modal-cancel">Annulla</button>
<button type="button" class="regia-btn regia-btn--yellow" id="modal-confirm"><%= t("regia.modal.confirm") %></button>
<button type="button" class="regia-btn regia-btn--outline" id="modal-cancel"><%= t("regia.modal.cancel") %></button>
</div>
</div>
</div>
@@ -1,18 +1,18 @@
<% content_for :title, "Registra la squadra — Match Live TV" %>
<% content_for :meta_description, "Iscrivi la tua società sportiva giovanile su Match Live TV: dirette live da telefono, link per genitori e piani Free o Premium." %>
<% content_for :title, t("auth.signup.meta_title") %>
<% content_for :meta_description, t("auth.signup.meta_description") %>
<% content_for :robots, "noindex, follow" %>
<section class="auth-page">
<h1>Registra la squadra</h1>
<p class="auth-lead">Crea l'account del referente (coach o dirigente), poi configurerai la squadra.</p>
<h1><%= t("auth.signup.title") %></h1>
<p class="auth-lead"><%= t("auth.signup.lead") %></p>
<div class="card">
<%= form_with model: @user, url: public_signup_path do |f| %>
<%= f.label :name, "Nome" %>
<%= f.label :name, t("auth.signup.name_label") %>
<%= f.text_field :name, required: true %>
<%= render "shared/input_toggle",
name: "user[email]",
id: "user_email",
label: "Email",
label: t("auth.email"),
value: @user.email,
input_type: "email",
required: true,
@@ -20,7 +20,7 @@
<%= render "shared/input_toggle",
name: "user[password]",
id: "user_password",
label: "Password",
label: t("auth.password"),
input_type: "text",
required: true,
minlength: 8,
@@ -28,24 +28,25 @@
<%= render "shared/input_toggle",
name: "user[password_confirmation]",
id: "user_password_confirmation",
label: "Conferma password",
label: t("auth.password_confirmation"),
input_type: "text",
required: true,
autocomplete: "new-password" %>
<label class="legal-accept">
<%= check_box_tag :accept_terms, "1", false, required: true %>
<span class="legal-accept-text">
Ho letto e accetto l<%= link_to "informativa privacy", public_privacy_path, target: "_blank", rel: "noopener" %>
e i <%= link_to "termini di servizio", public_termini_path, target: "_blank", rel: "noopener" %>.
<%= raw t(
"auth.signup.legal_accept_html",
privacy_link: link_to(t("auth.signup.privacy_link"), public_privacy_path, target: "_blank", rel: "noopener"),
terms_link: link_to(t("auth.signup.terms_link"), public_termini_path, target: "_blank", rel: "noopener")
) %>
</span>
</label>
<p class="legal-accept-hint">
Registrandoti dichiari di essere maggiorenne e, se operi per una società sportiva con atleti minorenni,
di avere legittima facoltà (es. dirigente, coach autorizzato) e di rispettare le regole su immagini dei minori
descritte in privacy.
<%= t("auth.signup.legal_hint") %>
</p>
<%= f.submit "Continua", class: "btn btn-primary" %>
<%= f.submit t("auth.signup.submit"), class: "btn btn-primary" %>
<% end %>
<p class="auth-footer">Hai già un account? <%= link_to "Accedi", public_login_path %></p>
<p class="auth-footer"><%= raw t("auth.signup.has_account_html", login_link: link_to(t("auth.signup.login_link"), public_login_path)) %></p>
</div>
</section>
+18 -18
View File
@@ -1,11 +1,11 @@
<% content_for :title, "Replay — Match Live TV" %>
<% content_for :meta_description, "Archivio replay delle dirette sportive su Match Live TV. Cerca per società, squadra o avversario." %>
<% content_for :title, t("replay.index.meta_title") %>
<% content_for :meta_description, t("replay.index.meta_description") %>
<div class="wrap replay-index">
<%= link_to "← Dirette live", public_live_index_path, class: "back-link" %>
<%= link_to t("replay.index.back_to_live"), public_live_index_path, class: "back-link" %>
<h1>Live passate</h1>
<p class="results-hint">Replay pubblici delle società sportive — riguarda le partite già trasmesse.</p>
<h1><%= t("replay.index.title") %></h1>
<p class="results-hint"><%= t("replay.index.hint") %></p>
<%= form_with url: public_replay_index_path, method: :get, local: true, class: "search-form" do %>
<% if params[:club_id].present? %>
@@ -15,13 +15,13 @@
type="search"
name="q"
value="<%= @query %>"
placeholder="Es. Tigers Volley, avversario, società, luogo…"
aria-label="Cerca replay"
placeholder="<%= t("replay.index.search_placeholder") %>"
aria-label="<%= t("replay.index.search_aria_label") %>"
autocomplete="off"
/>
<button type="submit" class="btn btn-primary">Cerca</button>
<button type="submit" class="btn btn-primary"><%= t("replay.index.search_button") %></button>
<% if @query.present? || params[:club_id].present? %>
<%= link_to "Azzera", public_replay_index_path, class: "btn btn-secondary" %>
<%= link_to t("replay.index.reset_link"), public_replay_index_path, class: "btn btn-secondary" %>
<% end %>
<% end %>
@@ -30,9 +30,9 @@
<% if @query.present? %>
<%= hidden_field_tag :q, @query %>
<% end %>
<label for="club_id" class="muted">Società</label>
<label for="club_id" class="muted"><%= t("replay.index.club_filter_label") %></label>
<select name="club_id" id="club_id" class="input" onchange="this.form.submit()">
<option value="">Tutte le società</option>
<option value=""><%= t("replay.index.club_filter_all") %></option>
<% @clubs.each do |club| %>
<option value="<%= club.id %>"<%= " selected" if params[:club_id].to_s == club.id.to_s %>><%= club.name %></option>
<% end %>
@@ -41,9 +41,9 @@
<% end %>
<% if @query.present? %>
<% club_suffix = @filter_club ? t("replay.index.results_hint_club_suffix", name: @filter_club.name) : "" %>
<p class="results-hint">
Risultati per «<%= h @query %>»<% if @filter_club %> · <%= @filter_club.name %><% end %>:
<%= @recordings.size %> replay
<%= t("replay.index.results_hint", query: @query, club_suffix: club_suffix, count: @recordings.size) %>
</p>
<% end %>
@@ -81,12 +81,12 @@
<% else %>
<div class="empty-state">
<% if @query.present? || params[:club_id].present? %>
<p><strong>Nessun replay trovato</strong> con i filtri selezionati.</p>
<p><%= link_to "Mostra tutti i replay pubblici", public_replay_index_path, class: "btn btn-secondary" %></p>
<p><strong><%= t("replay.index.empty_filtered_title") %></strong><%= t("replay.index.empty_filtered_suffix") %></p>
<p><%= link_to t("replay.index.empty_filtered_show_all"), public_replay_index_path, class: "btn btn-secondary" %></p>
<% else %>
<p><strong>Nessun replay pubblico al momento.</strong></p>
<p>Quando una società rende pubblica una registrazione, comparirà qui.</p>
<%= link_to "Vai alle dirette live", public_live_index_path, class: "btn btn-secondary", style: "margin-top:12px;display:inline-block" %>
<p><strong><%= t("replay.index.empty_default_title") %></strong></p>
<p><%= t("replay.index.empty_default_body") %></p>
<%= link_to t("replay.index.empty_default_live_link"), public_live_index_path, class: "btn btn-secondary", style: "margin-top:12px;display:inline-block" %>
<% end %>
</div>
<% end %>
+21 -21
View File
@@ -1,18 +1,18 @@
<% club_name = @match.team.club&.name %>
<% content_for :title, "Replay#{@recording.title_or_default}" %>
<% content_for :meta_description, "Replay #{@recording.title_or_default} su Match Live TV." %>
<% content_for :title, t("replay.show.title", title: @recording.title_or_default) %>
<% content_for :meta_description, t("replay.show.meta_description", title: @recording.title_or_default) %>
<% content_for :robots, @recording.publicly_listed? ? "index, follow" : "noindex, nofollow" %>
<div class="wrap">
<%= link_to "← Live passate", public_replay_index_path, class: "back-link" %>
<%= link_to t("replay.show.back_to_replays"), public_replay_index_path, class: "back-link" %>
<%= live_match_page_heading(@match) %>
<% if @recording.status == "processing" %>
<div class="stream-ended" role="status">
<div class="stream-ended-icon" aria-hidden="true"></div>
<h2>Replay in elaborazione</h2>
<p>La registrazione viene preparata. Riceverai unemail quando sarà pronta.</p>
<h2><%= t("replay.show.processing_title") %></h2>
<p><%= t("replay.show.processing_body") %></p>
</div>
<% elsif @recording.ready? && @recording.storage_key.present? %>
<div class="live-player-wrap">
@@ -25,18 +25,18 @@
session: @session,
stream_closed: true,
on_air: false,
badge_label: "REPLAY",
badge_label: t("replay.show.badge_label"),
badge_class: "badge-ended" %>
</div>
<p class="replay-show__meta-line">
<%= l_local(@recording.recorded_at_or_fallback) %>
· Durata <%= @recording.duration_label %>
· <%= t("replay.show.meta_line_duration", value: @recording.duration_label) %>
· <%= @recording.views_label %>
<% if @recording.expires_at %>
· Fino al <%= l_local(@recording.expires_at) %>
· <%= t("replay.show.meta_line_until", date: l_local(@recording.expires_at)) %>
<% end %>
· <%= @recording.publicly_listed? ? "Pubblico" : "Privato (link)" %>
· <%= @recording.publicly_listed? ? t("replay.show.meta_line_public") : t("replay.show.meta_line_private") %>
<% if @recording.source_platform_label != "—" %>
· <%= @recording.source_platform_label %>
<% end %>
@@ -46,10 +46,10 @@
<% if (ent.phone_download_enabled? && (logged_in? || @recording.unlisted?)) || @recording.youtube_watch_url %>
<div class="replay-show__actions">
<% if ent.phone_download_enabled? && (logged_in? || @recording.unlisted?) %>
<%= link_to "Scarica MP4", public_replay_download_path(@session), class: "btn btn-primary" %>
<%= link_to t("replay.show.download_link"), public_replay_download_path(@session), class: "btn btn-primary" %>
<% end %>
<% if @recording.youtube_watch_url %>
<%= link_to "Apri su YouTube", @recording.youtube_watch_url, class: "btn btn-secondary", target: "_blank", rel: "noopener" %>
<%= link_to t("replay.show.youtube_link"), @recording.youtube_watch_url, class: "btn btn-secondary", target: "_blank", rel: "noopener" %>
<% end %>
</div>
<% end %>
@@ -65,28 +65,28 @@
session: @session,
stream_closed: true,
on_air: false,
badge_label: "REPLAY",
badge_label: t("replay.show.badge_label"),
badge_class: "badge-ended" %>
</div>
<p class="replay-show__meta-line">
Replay su YouTube (copia server non presente).
<%= link_to "Apri su YouTube", @recording.youtube_watch_url, target: "_blank", rel: "noopener" %>
<%= t("replay.show.youtube_only_body") %>
<%= link_to t("replay.show.youtube_link"), @recording.youtube_watch_url, target: "_blank", rel: "noopener" %>
</p>
<% elsif @recording.ready? %>
<div class="stream-ended" role="status">
<h2>File non disponibile</h2>
<p>Il video non è più sul server (archivio rimosso o in migrazione).</p>
<p class="stream-ended-meta">Durata registrata: <%= @recording.duration_label %> · <%= @recording.byte_size_label %></p>
<h2><%= t("replay.show.file_missing_title") %></h2>
<p><%= t("replay.show.file_missing_body") %></p>
<p class="stream-ended-meta"><%= t("replay.show.file_missing_meta", duration: @recording.duration_label, size: @recording.byte_size_label) %></p>
</div>
<% elsif @recording.status == "failed" %>
<div class="stream-ended" role="status">
<h2>Replay non disponibile</h2>
<p><%= @recording.error_message.presence || "Errore di elaborazione della registrazione." %></p>
<h2><%= t("replay.show.failed_title") %></h2>
<p><%= @recording.error_message.presence || t("replay.show.failed_default_body") %></p>
</div>
<% else %>
<div class="stream-ended" role="status">
<h2>Replay non disponibile</h2>
<p>Questa registrazione non è più accessibile.</p>
<h2><%= t("replay.show.unavailable_title") %></h2>
<p><%= t("replay.show.unavailable_body") %></p>
</div>
<% end %>
</div>
@@ -1,26 +1,26 @@
<% content_for :title, "Accedi — Match Live TV" %>
<% content_for :meta_description, "Accesso area riservata per staff e società registrate su Match Live TV." %>
<% content_for :title, t("auth.login.meta_title") %>
<% content_for :meta_description, t("auth.login.meta_description") %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>Accedi</h1>
<h1><%= t("auth.login.title") %></h1>
<div class="card">
<%= form_with url: public_login_path do |f| %>
<%= render "shared/input_toggle",
name: :email,
label: "Email",
label: t("auth.email"),
value: params[:email],
input_type: "email",
required: true,
autocomplete: "username" %>
<%= render "shared/input_toggle",
name: :password,
label: "Password",
label: t("auth.password"),
required: true,
autocomplete: "current-password" %>
<p class="auth-forgot"><%= link_to "Password dimenticata?", public_password_forgot_path %></p>
<%= submit_tag "Entra", class: "btn btn-primary" %>
<p class="auth-forgot"><%= link_to t("auth.login.forgot_password"), public_password_forgot_path %></p>
<%= submit_tag t("auth.login.submit"), class: "btn btn-primary" %>
<% end %>
<p class="auth-footer">Nuovo? <%= link_to "Registrati", public_signup_path %></p>
<p class="auth-footer"><%= raw t("auth.login.new_prompt_html", signup_link: link_to(t("auth.login.signup_link"), public_signup_path)) %></p>
</div>
</section>
@@ -1,12 +1,11 @@
<% content_for :title, "Squadre su Match Live TV — Dirette e replay sport giovanili" %>
<% content_for :meta_description, "Scopri le squadre sportive giovanili che trasmettono su Match Live TV. Cerca per nome, filtra per sport, trova dirette in corso e archivi replay." %>
<% content_for :title, t("team_pages.index.meta_title") %>
<% content_for :meta_description, t("team_pages.index.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_team_pages_path(filter_params_for_canonical)) %>
<div class="wrap team-directory">
<h1>Squadre su Match Live TV</h1>
<h1><%= t("team_pages.index.heading") %></h1>
<p class="results-hint">
Società e squadre con dirette, replay pubblici o partite in programma.
Cerca la tua squadra e apri la pagina per seguire calendario e archivio.
<%= t("team_pages.index.hint") %>
</p>
<%= form_with url: public_team_pages_path, method: :get, local: true, class: "team-directory-filters" do %>
@@ -15,18 +14,18 @@
type="search"
name="q"
value="<%= @query %>"
placeholder="Nome squadra, società, città o palestra…"
aria-label="Cerca squadra"
placeholder="<%= t("team_pages.index.search_placeholder") %>"
aria-label="<%= t("team_pages.index.search_aria_label") %>"
autocomplete="off"
/>
<button type="submit" class="btn btn-primary">Cerca</button>
<button type="submit" class="btn btn-primary"><%= t("team_pages.index.search_button") %></button>
</div>
<div class="team-directory-filters__row">
<div class="team-directory-filters__field">
<label for="sport" class="team-directory-filters__label">Sport</label>
<label for="sport" class="team-directory-filters__label"><%= t("team_pages.index.sport_label") %></label>
<select name="sport" id="sport" class="team-directory-filters__select" onchange="this.form.requestSubmit()">
<option value="">Tutti gli sport</option>
<option value=""><%= t("team_pages.index.sport_all") %></option>
<% @sport_options.each do |sport| %>
<option value="<%= sport[:key] %>"<%= " selected" if @sport == sport[:key] %>><%= sport[:label] %></option>
<% end %>
@@ -34,20 +33,20 @@
</div>
<div class="team-directory-filters__field team-directory-filters__field--chips">
<span class="team-directory-filters__label" id="team-filter-chips-label">Mostra</span>
<span class="team-directory-filters__label" id="team-filter-chips-label"><%= t("team_pages.index.chips_label") %></span>
<div class="team-directory-chips" role="group" aria-labelledby="team-filter-chips-label">
<label class="filter-chip<%= " is-active" if @live_filter %>">
<%= check_box_tag :live, "1", @live_filter, id: "filter_live", class: "filter-chip__input", onchange: "this.form.requestSubmit()" %>
<span class="filter-chip__face" aria-hidden="true">
<span class="filter-chip__dot filter-chip__dot--live"></span>
In diretta
<%= t("team_pages.index.chip_live") %>
</span>
</label>
<label class="filter-chip<%= " is-active" if @replays_filter %>">
<%= check_box_tag :replays, "1", @replays_filter, id: "filter_replays", class: "filter-chip__input", onchange: "this.form.requestSubmit()" %>
<span class="filter-chip__face" aria-hidden="true">
<span class="filter-chip__icon"></span>
Con replay
<%= t("team_pages.index.chip_replays") %>
</span>
</label>
</div>
@@ -56,7 +55,7 @@
<% if @query.present? || @sport.present? || @live_filter || @replays_filter %>
<div class="team-directory-filters__actions">
<span class="team-directory-filters__label team-directory-filters__label--spacer" aria-hidden="true">&nbsp;</span>
<%= link_to "Azzera", public_team_pages_path, class: "btn btn-secondary team-directory-filters__reset" %>
<%= link_to t("team_pages.index.reset_link"), public_team_pages_path, class: "btn btn-secondary team-directory-filters__reset" %>
</div>
<% end %>
</div>
@@ -64,12 +63,12 @@
<% if @query.present? || @sport.present? || @live_filter || @replays_filter %>
<p class="results-hint">
<%= @entries.size %> squadre trovate
<% if @query.present? %> per «<%= h @query %>»<% end %>
<% if @sport.present? %> · <%= Sports::Catalog.find_optional(@sport)&.dig(:label) || @sport %><% end %>
<%= t("team_pages.index.results_found", count: @entries.size) %>
<% if @query.present? %><%= t("team_pages.index.results_query_suffix", query: @query) %><% end %>
<% if @sport.present? %><%= t("team_pages.index.results_sport_suffix", sport: Sports::Catalog.find_optional(@sport)&.dig(:label) || @sport) %><% end %>
</p>
<% else %>
<p class="results-hint"><%= @entries.size %> squadre attive</p>
<p class="results-hint"><%= t("team_pages.index.results_active", count: @entries.size) %></p>
<% end %>
<% if @entries.any? %>
@@ -96,13 +95,13 @@
<p class="team-directory-card__meta"><%= team.sport_label %></p>
<div class="team-directory-card__badges">
<% if entry.live_now %>
<span class="badge badge-on-air">In diretta</span>
<span class="badge badge-on-air"><%= t("team_pages.index.badge_live") %></span>
<% end %>
<% if entry.upcoming_count.positive? %>
<span class="badge badge-scheduled"><%= entry.upcoming_count %> in programma</span>
<span class="badge badge-scheduled"><%= t("team_pages.index.badge_upcoming", count: entry.upcoming_count) %></span>
<% end %>
<% if entry.replay_count.positive? %>
<span class="badge badge-wait"><%= entry.replay_count %> replay</span>
<span class="badge badge-wait"><%= t("team_pages.index.badge_replay", count: entry.replay_count) %></span>
<% end %>
</div>
</div>
@@ -112,18 +111,18 @@
<% else %>
<div class="empty-state">
<% if @query.present? || @sport.present? || @live_filter || @replays_filter %>
<p><strong>Nessuna squadra trovata</strong> con i filtri selezionati.</p>
<p><%= link_to "Mostra tutte le squadre attive", public_team_pages_path, class: "btn btn-secondary" %></p>
<p><strong><%= t("team_pages.index.empty_filtered_title") %></strong><%= t("team_pages.index.empty_filtered_suffix") %></p>
<p><%= link_to t("team_pages.index.empty_filtered_show_all"), public_team_pages_path, class: "btn btn-secondary" %></p>
<% else %>
<p><strong>Nessuna squadra attiva al momento.</strong></p>
<p>Quando una società avvierà dirette o pubblicherà replay, comparirà qui.</p>
<%= link_to "Vai alle dirette live", public_live_index_path, class: "btn btn-secondary", style: "margin-top:12px;display:inline-block" %>
<p><strong><%= t("team_pages.index.empty_default_title") %></strong></p>
<p><%= t("team_pages.index.empty_default_body") %></p>
<%= link_to t("team_pages.index.empty_default_live_link"), public_live_index_path, class: "btn btn-secondary", style: "margin-top:12px;display:inline-block" %>
<% end %>
</div>
<% end %>
<p class="team-directory-footer muted">
<%= link_to "Dirette in corso", public_live_index_path %> ·
<%= link_to "Archivio replay", public_replay_index_path %>
<%= link_to t("team_pages.index.footer_live_link"), public_live_index_path %> ·
<%= link_to t("team_pages.index.footer_replay_link"), public_replay_index_path %>
</p>
</div>
@@ -1,4 +1,4 @@
<% content_for :title, "#{@team.name}#{@club.name} | Match Live TV" %>
<% content_for :title, t("team_pages.show.title", team: @team.name, club: @club.name) %>
<% content_for :meta_description, team_page_meta_description(@team, @club) %>
<% content_for :canonical_url, seo_absolute_url(public_team_page_path(@team.slug)) %>
<% content_for :head do %>
@@ -9,7 +9,7 @@
<div class="wrap team-public-page">
<nav class="team-dashboard-nav" aria-label="Percorso">
<%= link_to "← Tutte le squadre", public_team_pages_path, class: "team-dashboard-nav__club" %>
<%= link_to t("team_pages.show.breadcrumb_all_teams"), public_team_pages_path, class: "team-dashboard-nav__club" %>
</nav>
<header class="roster-hero card team-public-hero" style="--club-primary:<%= @team.effective_primary_color %>;--club-secondary:<%= @team.effective_secondary_color %>">
@@ -19,7 +19,7 @@
<p class="team-public-meta">
<%= @team.sport_label %>
<% if @live_sessions.any? %>
· <span class="hero-live-dot" aria-hidden="true"></span> <strong>In diretta ora</strong>
· <span class="hero-live-dot" aria-hidden="true"></span> <strong><%= t("team_pages.show.live_now_label") %></strong>
<% end %>
</p>
<% if @team.description.present? %>
@@ -27,11 +27,11 @@
<% end %>
<div class="team-public-actions">
<% if @live_sessions.any? %>
<%= link_to "Guarda la diretta", public_live_path(@live_sessions.first), class: "btn btn-primary" %>
<%= link_to t("team_pages.show.watch_live_link"), public_live_path(@live_sessions.first), class: "btn btn-primary" %>
<% end %>
<%= link_to "Tutte le dirette", public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
<%= link_to t("team_pages.show.all_live_link"), public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
<% if @recordings.any? %>
<%= link_to "Archivio replay", public_replay_index_path(team_id: @team.id), class: "btn btn-secondary" %>
<%= link_to t("team_pages.show.replay_archive_link"), public_replay_index_path(team_id: @team.id), class: "btn btn-secondary" %>
<% end %>
</div>
</div>
@@ -51,7 +51,7 @@
<% if @live_sessions.any? %>
<section class="team-public-section" aria-labelledby="team-live-heading">
<h2 id="team-live-heading" class="section-heading">In diretta adesso</h2>
<h2 id="team-live-heading" class="section-heading"><%= t("team_pages.show.section_live_now") %></h2>
<div class="live-grid">
<% @live_sessions.each do |session| %>
<% match = session.match %>
@@ -66,21 +66,21 @@
<p class="card-score">
<span class="card-sets"><%= live_score_sets_label(session.score_state, match) %></span>
<% if live_score_partials_label(session.score_state).present? %>
<span class="card-partials">Parziali: <%= live_score_partials_label(session.score_state) %></span>
<span class="card-partials"><%= t("score.partials_prefix", value: live_score_partials_label(session.score_state)) %></span>
<% end %>
<span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span>
</p>
<% end %>
<div class="badges">
<% if on_air %>
<span class="badge badge-on-air">In onda</span>
<span class="badge badge-on-air"><%= t("live.index.badge_on_air") %></span>
<% elsif session.paused? %>
<span class="badge badge-connecting">In pausa</span>
<span class="badge badge-connecting"><%= t("live.index.badge_paused") %></span>
<% else %>
<span class="badge badge-live">Live</span>
<span class="badge badge-live"><%= t("live.index.badge_live") %></span>
<% end %>
</div>
<%= link_to "Guarda diretta →", public_live_path(session), class: "btn-watch" %>
<%= link_to t("live.index.watch_link"), public_live_path(session), class: "btn-watch" %>
</article>
<% end %>
</div>
@@ -89,8 +89,8 @@
<% if @upcoming_matches.any? %>
<section class="team-public-section" aria-labelledby="team-upcoming-heading">
<h2 id="team-upcoming-heading" class="section-heading<%= " section-heading--spaced" if @live_sessions.any? %>">Prossime partite</h2>
<p class="results-hint">Programmate dal club: la diretta partirà quando lo staff avvierà la trasmissione.</p>
<h2 id="team-upcoming-heading" class="section-heading<%= " section-heading--spaced" if @live_sessions.any? %>"><%= t("team_pages.show.section_upcoming") %></h2>
<p class="results-hint"><%= t("team_pages.show.upcoming_hint") %></p>
<div class="live-grid upcoming-grid">
<% @upcoming_matches.each do |match| %>
<article class="live-card live-card--upcoming">
@@ -103,7 +103,7 @@
</p>
<p class="upcoming-when"><%= live_scheduled_relative(match.scheduled_at) %></p>
<div class="badges">
<span class="badge badge-scheduled">In programma</span>
<span class="badge badge-scheduled"><%= t("live.index.badge_scheduled") %></span>
</div>
</article>
<% end %>
@@ -113,7 +113,7 @@
<% if @recordings.any? %>
<section class="team-public-section" aria-labelledby="team-replays-heading">
<h2 id="team-replays-heading" class="section-heading section-heading--spaced">Replay recenti</h2>
<h2 id="team-replays-heading" class="section-heading section-heading--spaced"><%= t("team_pages.show.section_replays") %></h2>
<div class="replay-grid">
<% @recordings.each do |rec| %>
<% match = rec.stream_session.match %>
@@ -137,21 +137,21 @@
<% end %>
</div>
<p class="team-public-more">
<%= link_to "Vedi tutti i replay di #{@team.name} →", public_replay_index_path(team_id: @team.id) %>
<%= link_to t("team_pages.show.replays_more_link", team: @team.name), public_replay_index_path(team_id: @team.id) %>
</p>
</section>
<% end %>
<% if @roster_by_category %>
<section class="team-public-section" aria-labelledby="team-roster-heading">
<h2 id="team-roster-heading" class="section-heading section-heading--spaced">Organico</h2>
<h2 id="team-roster-heading" class="section-heading section-heading--spaced"><%= t("team_pages.show.section_roster") %></h2>
<div class="team-public-roster">
<% TeamRosterMember::DISPLAY_ORDER.each do |category| %>
<% members = @roster_by_category[category] %>
<% next if members.blank? %>
<section class="roster-section card">
<header class="roster-section__head">
<h3 class="roster-section__title"><%= TeamRosterMember::CATEGORY_LABELS[category] %></h3>
<h3 class="roster-section__title"><%= TeamRosterMember.category_label(category) %></h3>
<span class="roster-section__count"><%= members.size %></span>
</header>
<div class="roster-list">
@@ -167,9 +167,9 @@
<% if @live_sessions.empty? && @upcoming_matches.empty? && @recordings.empty? %>
<div class="empty-state empty-state--soft team-public-empty">
<p><strong>Nessuna diretta o replay al momento.</strong></p>
<p>Torna a controllare prima delle prossime gare: qui compariranno dirette, calendario e archivio della squadra.</p>
<%= link_to "Vedi tutte le dirette", public_live_index_path, class: "btn btn-secondary" %>
<p><strong><%= t("team_pages.show.empty_title") %></strong></p>
<p><%= t("team_pages.show.empty_body") %></p>
<%= link_to t("team_pages.show.empty_link"), public_live_index_path, class: "btn btn-secondary" %>
</div>
<% end %>
</div>
@@ -1,8 +1,8 @@
<% content_for :title, "Modifica — #{@member.full_name}" %>
<% content_for :title, t("roster.edit.title", name: @member.full_name) %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:520px">
<p class="muted"><%= link_to "← Dettagli squadra", public_team_details_path(@team) %></p>
<p class="muted"><%= link_to t("roster.back_to_details"), public_team_details_path(@team) %></p>
<h1><%= @member.full_name %></h1>
<%= render "shared/roster_member_form", member: @member, team: @team %>
</div>

Some files were not shown because too many files have changed in this diff Show More