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) account = AdminAccount.find_by(username: params[:username]&.strip)
if account&.authenticate(params[:password]) if account&.authenticate(params[:password])
session[:admin_account_id] = account.id 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 else
flash.now[:alert] = "Username o password non validi" flash.now[:alert] = t("admin.flash.invalid_credentials")
render :new, status: :unauthorized render :new, status: :unauthorized
end end
end end
def destroy def destroy
reset_session reset_session
redirect_to admin_login_path, notice: "Disconnesso" redirect_to admin_login_path, notice: t("admin.flash.logout_success")
end end
end end
end end
@@ -3,20 +3,40 @@ module Admin
layout "admin" layout "admin"
protect_from_forgery with: :exception protect_from_forgery with: :exception
before_action :set_admin_locale
before_action :require_admin_login before_action :require_admin_login
include ::AdminHelper include ::AdminHelper
helper ApplicationHelper helper ApplicationHelper
helper RecordingsArchiveHelper 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 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 def require_admin_login
return if admin_logged_in? return if admin_logged_in?
session[:admin_return_to] = request.fullpath unless request.path == admin_login_path 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 end
def current_admin_account def current_admin_account
@@ -11,7 +11,7 @@ module Admin
payment = Billing::Payment.find(params[:payment_id]) payment = Billing::Payment.find(params[:payment_id])
Billing::AttachPaymentInvoice.call(payment: payment, pdf: params[:pdf]) Billing::AttachPaymentInvoice.call(payment: payment, pdf: params[:pdf])
redirect_to admin_billing_path(billing_redirect_params(payment)), 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 rescue Billing::AttachPaymentInvoice::Error, Billing::IssueInvoice::Error => e
payment ||= Billing::Payment.find_by(id: params[:payment_id]) payment ||= Billing::Payment.find_by(id: params[:payment_id])
redirect_to admin_billing_path(payment ? billing_redirect_params(payment) : {}), redirect_to admin_billing_path(payment ? billing_redirect_params(payment) : {}),
@@ -8,7 +8,7 @@ module Admin
end end
def new 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 end
def create def create
@@ -18,7 +18,7 @@ module Admin
if @invoice.save if @invoice.save
redirect_to edit_admin_club_billing_invoice_path(@club, @invoice), 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 else
@payment = @invoice.billing_payment @payment = @invoice.billing_payment
flash.now[:alert] = @invoice.errors.full_messages.join(", ") flash.now[:alert] = @invoice.errors.full_messages.join(", ")
@@ -36,9 +36,9 @@ module Admin
if issuing? if issuing?
Billing::IssueInvoice.call(invoice: @invoice, pdf: params.dig(:billing_invoice, :pdf)) Billing::IssueInvoice.call(invoice: @invoice, pdf: params.dig(:billing_invoice, :pdf))
redirect_to admin_billing_path(club_id: @club.id), 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 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 else
@payment = @invoice.billing_payment @payment = @invoice.billing_payment
flash.now[:alert] = @invoice.errors.full_messages.join(", ") flash.now[:alert] = @invoice.errors.full_messages.join(", ")
@@ -61,7 +61,7 @@ module Admin
end end
def issuing? def issuing?
params[:commit].to_s == "Emetti e invia via email" params[:commit_action].to_s == "issue"
end end
def invoice_params def invoice_params
@@ -20,14 +20,14 @@ module Admin
reason: params[:reason], reason: params[:reason],
admin: current_admin_account 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 rescue Billing::AdminCompedSubscription::Error, ActiveRecord::RecordInvalid => e
redirect_back_or_club alert: e.message redirect_back_or_club alert: e.message
end end
def revoke_comped def revoke_comped
Billing::AdminCompedSubscription.revoke(club: @club, admin: current_admin_account) 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 rescue Billing::AdminCompedSubscription::Error => e
redirect_back_or_club alert: e.message redirect_back_or_club alert: e.message
end 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 def acknowledge
incident = Ops::Incident.find(params[:id]) incident = Ops::Incident.find(params[:id])
incident.acknowledge! incident.acknowledge!
redirect_to admin_ops_path, notice: "Incidente preso in carico" redirect_to admin_ops_path, notice: t("admin.flash.ops_acknowledged")
end end
def resolve def resolve
incident = Ops::Incident.find(params[:id]) incident = Ops::Incident.find(params[:id])
incident.resolve! incident.resolve!
redirect_to admin_ops_path, notice: "Incidente risolto" redirect_to admin_ops_path, notice: t("admin.flash.ops_resolved")
end end
def mute def mute
incident = Ops::Incident.find(params[:id]) incident = Ops::Incident.find(params[:id])
incident.mute!(duration: 24.hours) 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 end
private private
@@ -5,22 +5,22 @@ module Admin
def update def update
unless current_admin_account.authenticate(params[:current_password]) 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 return render :edit, status: :unprocessable_entity
end end
if params[:password].blank? || params[:password].length < 8 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 return render :edit, status: :unprocessable_entity
end end
if params[:password] != params[:password_confirmation] 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 return render :edit, status: :unprocessable_entity
end end
if current_admin_account.update(password: params[:password]) 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 else
flash.now[:alert] = current_admin_account.errors.full_messages.to_sentence flash.now[:alert] = current_admin_account.errors.full_messages.to_sentence
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
@@ -12,28 +12,28 @@ module Admin
def stop def stop
@session = StreamSession.find(params[:id]) @session = StreamSession.find(params[:id])
if @session.terminal? 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 return
end end
Sessions::Stop.new(@session).call 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 rescue StandardError => e
Rails.logger.error("[admin] stop session #{@session.id}: #{e.class} #{e.message}") 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 end
def regia_link def regia_link
@session = StreamSession.find(params[:id]) @session = StreamSession.find(params[:id])
if @session.terminal? 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 return
end end
access = Sessions::RegiaAccess.new(@session) access = Sessions::RegiaAccess.new(@session)
token = access.issue_token! token = access.issue_token!
redirect_to admin_session_path(@session), redirect_to admin_session_path(@session),
notice: "Link regia generato.", notice: t("admin.flash.session_regia_generated"),
flash: { flash: {
regia_url: access.url(token), regia_url: access.url(token),
regia_expires_at: @session.regia_token_expires_at&.iso8601 regia_expires_at: @session.regia_token_expires_at&.iso8601
@@ -2,7 +2,7 @@ module Admin
class YoutubeController < Admin::BaseController class YoutubeController < Admin::BaseController
def platform def platform
if ENV["YOUTUBE_CLIENT_ID"].blank? 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 return
end end
@@ -15,9 +15,9 @@ module Public
@club, @club,
plan: premium_checkout_return_params[:plan], plan: premium_checkout_return_params[:plan],
interval: premium_checkout_return_params[:interval] interval: premium_checkout_return_params[:interval]
), notice: "Dati salvati. Procedi con il pagamento." ), notice: t("flash.club_billing.profile_saved_proceed_payment")
else 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 end
else else
flash.now[:alert] = @club.errors.full_messages.join(", ") flash.now[:alert] = @club.errors.full_messages.join(", ")
@@ -27,25 +27,25 @@ module Public
def cancel_subscription def cancel_subscription
unless MatchLiveTv.stripe_enabled? 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 return
end end
Billing::Stripe::CancelSubscription.call(club: @club) Billing::Stripe::CancelSubscription.call(club: @club)
sub = @club.subscription.reload 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), 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 rescue ArgumentError, RuntimeError => e
redirect_to public_club_billing_path(@club), alert: e.message redirect_to public_club_billing_path(@club), alert: e.message
rescue ::Stripe::StripeError => e 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 end
def download_invoice def download_invoice
invoice = @club.billing_invoices.find(params[:invoice_id]) invoice = @club.billing_invoices.find(params[:invoice_id])
unless invoice.pdf.attached? 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 return
end end
@@ -14,7 +14,7 @@ module Public
@selected_team = @schedulable_teams.find { |t| t.id.to_s == params[:team_id].to_s } @selected_team = @schedulable_teams.find { |t| t.id.to_s == params[:team_id].to_s }
unless @selected_team unless @selected_team
@match = default_match_for_form @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 return render :new, status: :unprocessable_entity
end end
@@ -22,7 +22,7 @@ module Public
match_params.merge(sport: @selected_team.sport, sets_to_win: 3) match_params.merge(sport: @selected_team.sport, sets_to_win: 3)
) )
redirect_to public_live_index_path(club_id: @club.id), 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 rescue ActiveRecord::RecordInvalid => e
@match = @selected_team.matches.build(match_params) @match = @selected_team.matches.build(match_params)
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -44,7 +44,7 @@ module Public
return if @schedulable_teams.any? return if @schedulable_teams.any?
redirect_to public_live_index_path(club_id: @club.id), 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 end
def resolve_selected_team def resolve_selected_team
@@ -22,7 +22,7 @@ module Public
return if current_user.owned_clubs.exists?(id: @club.id) return if current_user.owned_clubs.exists?(id: @club.id)
return if current_user.manageable_teams.joins(:club).exists?(clubs: { 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 end
end end
@@ -12,7 +12,7 @@ module Public
def create def create
if current_user.owned_clubs.exists? 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 return
end end
@@ -22,7 +22,7 @@ module Public
ClubMembership.create!(user: current_user, club: club, role: "owner") 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!( club.teams.create!(
name: first_team_name, name: first_team_name,
sport: club.sport sport: club.sport
@@ -34,14 +34,14 @@ module Public
if plan.in?(%w[premium_light premium_full]) && MatchLiveTv.stripe_enabled? if plan.in?(%w[premium_light premium_full]) && MatchLiveTv.stripe_enabled?
unless club.billing_profile_complete? unless club.billing_profile_complete?
redirect_to public_club_billing_profile_path(club, plan: plan, interval: checkout_interval_param), 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 return
end end
interval = checkout_interval_param interval = checkout_interval_param
redirect_to public_club_checkout_path(club, plan: plan, interval: interval) redirect_to public_club_checkout_path(club, plan: plan, interval: interval)
else 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 end
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -66,7 +66,7 @@ module Public
@club.assign_attributes(club_params) @club.assign_attributes(club_params)
attach_branding_logo(@club) attach_branding_logo(@club)
@club.save! @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 rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
@@ -88,12 +88,12 @@ module Public
require_club_owner!(@club) require_club_owner!(@club)
if @club.subscription&.admin_comped? if @club.subscription&.admin_comped?
redirect_to public_club_billing_path(@club), 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 return
end end
unless MatchLiveTv.stripe_enabled? 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 return
end end
@@ -105,15 +105,15 @@ module Public
if sub&.stripe_subscription_id.present? && sub.active? && if sub&.stripe_subscription_id.present? && sub.active? &&
sub.plan.slug == plan_slug && sub.billing_interval == interval && !sub.plan_change_pending? sub.plan.slug == plan_slug && sub.billing_interval == interval && !sub.plan_change_pending?
redirect_to public_club_billing_path(@club), 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 return
end end
if sub&.plan_change_pending? && sub.pending_plan&.slug == plan_slug && if sub&.plan_change_pending? && sub.pending_plan&.slug == plan_slug &&
sub.pending_billing_interval == interval 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), 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 return
end end
@@ -134,7 +134,7 @@ module Public
redirect_to public_club_billing_path(@club), alert: e.message redirect_to public_club_billing_path(@club), alert: e.message
rescue ::Stripe::StripeError => e rescue ::Stripe::StripeError => e
Rails.logger.warn("[Stripe checkout] #{e.message}") 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 end
def youtube_connect def youtube_connect
@@ -143,7 +143,7 @@ module Public
entitlements.assert_can_connect_youtube! entitlements.assert_can_connect_youtube!
if ENV["YOUTUBE_CLIENT_ID"].blank? 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 return
end end
@@ -161,13 +161,13 @@ module Public
def youtube_disconnect def youtube_disconnect
require_club_owner!(@club) require_club_owner!(@club)
@club.youtube_credential&.destroy! @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 end
def portal def portal
require_club_owner!(@club) require_club_owner!(@club)
unless MatchLiveTv.stripe_enabled? 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 return
end end
@@ -204,7 +204,7 @@ module Public
@club, @club,
plan: plan_slug, plan: plan_slug,
interval: params[:interval].presence interval: params[:interval].presence
), alert: "Completa i dati di fatturazione prima di attivare un piano premium." ), alert: t("flash.clubs.complete_billing_first")
end end
def checkout_interval_param def checkout_interval_param
@@ -236,10 +236,10 @@ module Public
def apply_checkout_flash! def apply_checkout_flash!
case params[:checkout] case params[:checkout]
when "success" when "success"
plan_name = @subscription&.plan&.name || "premium" plan_name = @subscription&.plan&.name || "Premium"
flash.now[:notice] = "Pagamento completato! Piano attivo: #{plan_name}." flash.now[:notice] = t("flash.clubs.checkout_success", plan: plan_name)
when "canceled" when "canceled"
flash.now[:alert] = "Pagamento annullato. Nessun addebito è stato effettuato." flash.now[:alert] = t("flash.clubs.checkout_canceled")
end end
end end
end end
@@ -4,24 +4,24 @@ module Public
@token = params[:token] @token = params[:token]
@invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(@token.to_s)) @invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(@token.to_s))
unless @invitation 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
end end
def accept def accept
invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(params[:token].to_s)) 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 logged_in?
if current_user.email.downcase != invitation.email.downcase 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 return
end end
invitation.accept!(current_user) 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 else
session[:pending_invite_token] = params[:token] 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 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) match_params.merge(sport: @team.sport, sets_to_win: match_params[:sets_to_win].presence || 3)
) )
redirect_to public_team_matches_path(@team), 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 rescue ActiveRecord::RecordInvalid => e
@match = @team.matches.build(match_params) @match = @team.matches.build(match_params)
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -37,7 +37,7 @@ module Public
def update def update
@match.update!(match_params) @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 rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :edit, status: :unprocessable_entity render :edit, status: :unprocessable_entity
@@ -47,12 +47,12 @@ module Public
active = active_session_for(@match) active = active_session_for(@match)
if active&.resumable? if active&.resumable?
redirect_to public_team_matches_path(@team), 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 return
end end
@match.destroy! @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 end
private private
@@ -66,7 +66,7 @@ module Public
return if current_user.can_schedule_for?(@team) return if current_user.can_schedule_for?(@team)
redirect_to public_team_details_path(@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 end
def set_match def set_match
@@ -11,14 +11,14 @@ module Public
end end
redirect_to public_login_path, 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 end
def edit def edit
@user = User.find_by_password_reset_token(params[:token]) @user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired? if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path, 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 return
end end
@token = params[:token] @token = params[:token]
@@ -28,25 +28,25 @@ module Public
@user = User.find_by_password_reset_token(params[:token]) @user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired? if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path, 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 return
end end
if params[:password].blank? || params[:password].length < 8 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] @token = params[:token]
return render :edit, status: :unprocessable_entity return render :edit, status: :unprocessable_entity
end end
if params[:password] != params[:password_confirmation] 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] @token = params[:token]
return render :edit, status: :unprocessable_entity return render :edit, status: :unprocessable_entity
end end
@user.update!(password: params[:password]) @user.update!(password: params[:password])
@user.clear_password_reset! @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 end
end end
@@ -8,7 +8,7 @@ module Public
def create def create
unless params[:accept_terms] == "1" unless params[:accept_terms] == "1"
@user = User.new(user_params.merge(role: "coach")) @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 return render :new, status: :unprocessable_entity
end end
@@ -20,10 +20,10 @@ module Public
invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(token)) invitation = TeamInvitation.pending.find_by(token_digest: Digest::SHA256.hexdigest(token))
if invitation && invitation.email.downcase == @user.email.downcase if invitation && invitation.email.downcase == @user.email.downcase
invitation.accept!(@user) 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
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 else
flash.now[:alert] = @user.errors.full_messages.join(", ") flash.now[:alert] = @user.errors.full_messages.join(", ")
render :new, status: :unprocessable_entity render :new, status: :unprocessable_entity
@@ -42,7 +42,7 @@ module Public
def download def download
unless download_allowed? 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 return
end end
@@ -105,13 +105,13 @@ module Public
@recording = Recording.not_deleted.find_by(stream_session: @session) @recording = Recording.not_deleted.find_by(stream_session: @session)
return if @recording 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 end
def authorize_replay_access! def authorize_replay_access!
return if Recordings::AccessPolicy.new(@recording, viewer: current_user).allowed? 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 end
end end
@@ -17,16 +17,16 @@ module Public
else else
public_new_club_path public_new_club_path
end end
redirect_to dest, notice: "Bentornato!" redirect_to dest, notice: t("flash.sessions.welcome_back")
else else
flash.now[:alert] = "Email o password non validi" flash.now[:alert] = t("flash.sessions.invalid_credentials")
render :new, status: :unauthorized render :new, status: :unauthorized
end end
end end
def destroy def destroy
reset_session reset_session
redirect_to public_pricing_path, notice: "Disconnesso" redirect_to public_pricing_path, notice: t("flash.sessions.logged_out")
end end
end end
end end
@@ -9,7 +9,7 @@ module Public
member = @team.roster_members.build(member_params) member = @team.roster_members.build(member_params)
attach_photo(member) attach_photo(member)
member.save! 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 rescue ActiveRecord::RecordInvalid => e
redirect_to public_team_details_path(@team), alert: e.record.errors.full_messages.join(", ") redirect_to public_team_details_path(@team), alert: e.record.errors.full_messages.join(", ")
end end
@@ -22,7 +22,7 @@ module Public
@member.assign_attributes(member_params) @member.assign_attributes(member_params)
attach_photo(@member) attach_photo(@member)
@member.save! @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 rescue ActiveRecord::RecordInvalid => e
@club = @team.club @club = @team.club
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
@@ -32,7 +32,7 @@ module Public
def destroy def destroy
name = @member.full_name name = @member.full_name
@member.destroy! @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 end
private private
@@ -19,7 +19,7 @@ module Public
require_club_owner!(@club) require_club_owner!(@club)
team = @club.teams.create!(team_params) team = @club.teams.create!(team_params)
attach_branding_logo(team) 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 rescue ActiveRecord::RecordInvalid => e
flash.now[:alert] = e.record.errors.full_messages.join(", ") flash.now[:alert] = e.record.errors.full_messages.join(", ")
render :new, status: :unprocessable_entity render :new, status: :unprocessable_entity
@@ -49,7 +49,7 @@ module Public
attach_branding_logo(@team) attach_branding_logo(@team)
attach_team_photo(@team) attach_team_photo(@team)
@team.save! @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 rescue ActiveRecord::RecordInvalid => e
@club = @team.club @club = @team.club
flash.now[:alert] = e.record.errors.full_messages.join(", ") 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 = current_user.user_teams.find_or_initialize_by(team: @team)
membership.role = "member" if membership.new_record? membership.role = "member" if membership.new_record?
Teams::StaffAssignment.call(team: @team, user: current_user, membership: membership) 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 rescue Teams::StaffAssignmentError, Teams::EntitlementError => e
redirect_to public_team_details_path(@team), alert: e.message redirect_to public_team_details_path(@team), alert: e.message
end end
@@ -75,7 +75,7 @@ module Public
require_club_owner_for_team!(@team) require_club_owner_for_team!(@team)
membership = current_user.user_teams.find_by(team: @team) membership = current_user.user_teams.find_by(team: @team)
membership&.update!(staff_kind: nil) 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 end
def create_invitation def create_invitation
@@ -95,7 +95,7 @@ module Public
expires_at: 7.days.from_now expires_at: 7.days.from_now
) )
@invite_url = join_public_invitation_url(token: token) @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 render :invite
rescue Teams::EntitlementError, Teams::StaffAssignmentError => e rescue Teams::EntitlementError, Teams::StaffAssignmentError => e
redirect_to public_team_invite_path(@team), alert: e.message redirect_to public_team_invite_path(@team), alert: e.message
@@ -105,14 +105,14 @@ module Public
require_club_owner_for_team!(@team) require_club_owner_for_team!(@team)
ut = @team.user_teams.find_by!(user_id: params[:user_id], role: "member") ut = @team.user_teams.find_by!(user_id: params[:user_id], role: "member")
ut.destroy! 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 end
def destroy_invitation def destroy_invitation
require_club_owner_for_team!(@team) require_club_owner_for_team!(@team)
inv = @team.team_invitations.pending.find(params[:invitation_id]) inv = @team.team_invitations.pending.find(params[:invitation_id])
inv.destroy! 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 end
def youtube_connect def youtube_connect
@@ -125,7 +125,7 @@ module Public
def youtube_disconnect def youtube_disconnect
require_club_owner_for_team!(@team) require_club_owner_for_team!(@team)
@team.club.youtube_credential&.destroy! @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 end
private private
@@ -7,15 +7,15 @@ module Public
def require_login! def require_login!
return if logged_in? 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 end
def require_club_owner!(club) 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 end
def require_club_owner_for_team!(team) 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 end
end end
+1 -1
View File
@@ -23,7 +23,7 @@ module AdminHelper
def admin_session_watch_links(session) def admin_session_watch_links(session)
links = [] links = []
if session.matchlivetv_platform? 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 end
youtube = session.youtube_watch_url youtube = session.youtube_watch_url
links << { label: "YouTube", url: youtube } if youtube.present? links << { label: "YouTube", url: youtube } if youtube.present?
+28
View File
@@ -31,6 +31,34 @@ module ApplicationHelper
I18n.locale.to_s I18n.locale.to_s
end 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) def sport_catalog_options(selected = nil)
options = Sports::Catalog.as_api_list.map { |entry| [entry[:label], entry[:key]] } options = Sports::Catalog.as_api_list.map { |entry| [entry[:label], entry[:key]] }
options_for_select(options, Sports::Catalog.normalize_key(selected)) options_for_select(options, Sports::Catalog.normalize_key(selected))
+7 -7
View File
@@ -2,25 +2,25 @@ module Public
module BillingHelper module BillingHelper
def plan_billing_action(current_slug:, target_plan:, stripe_subscription_active:, current_interval: nil, subscription: nil) def plan_billing_action(current_slug:, target_plan:, stripe_subscription_active:, current_interval: nil, subscription: nil)
if target_plan.slug == "free" 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: :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 end
unless MatchLiveTv.stripe_enabled? unless MatchLiveTv.stripe_enabled?
return { kind: :disabled, label: "Stripe non configurato" } return { kind: :disabled, label: I18n.t("billing.actions.stripe_not_configured") }
end end
intervals = Billing::Stripe::PriceCatalog.available_intervals(plan_slug: target_plan.slug) 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 active_interval = current_interval.presence || Billing::Stripe::PriceCatalog::DEFAULT_INTERVAL
if current_slug == target_plan.slug && stripe_subscription_active && !subscription&.plan_change_pending? if current_slug == target_plan.slug && stripe_subscription_active && !subscription&.plan_change_pending?
other_intervals = intervals - [active_interval] other_intervals = intervals - [active_interval]
if other_intervals.empty? 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 } return { kind: :current, label: label }
end end
@@ -87,9 +87,9 @@ module Public
def billing_profile_incomplete_message(club) def billing_profile_incomplete_message(club)
missing = club.billing_profile_errors 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
end end
+16 -13
View File
@@ -8,16 +8,16 @@ module Public
when "basket", "timed" when "basket", "timed"
live_score_period_label(match, score_state) live_score_period_label(match, score_state)
when "timer" when "timer"
"Cronometro" t("score.timer_label")
when "generic" when "generic"
"Punteggio" t("score.generic_label")
else 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
end end
def live_score_period_label(match, score_state) def live_score_period_label(match, score_state)
return "" unless score_state return t("score.fallback") unless score_state
score_state.current_period_label score_state.current_period_label
end end
@@ -35,16 +35,16 @@ module Public
def live_scheduled_label(datetime) def live_scheduled_label(datetime)
return nil unless 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 end
def live_scheduled_relative(datetime) def live_scheduled_relative(datetime)
return nil unless datetime return nil unless datetime
if datetime.to_date == Time.zone.today 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 elsif datetime.to_date == Time.zone.tomorrow
"Domani alle #{datetime.strftime('%H:%M')}" t("score.scheduled_tomorrow", time: datetime.strftime("%H:%M"))
else else
live_scheduled_label(datetime) live_scheduled_label(datetime)
end end
@@ -52,7 +52,7 @@ module Public
def live_match_card_heading(match, link_team: true) def live_match_card_heading(match, link_team: true)
team = match.team 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_slug = team.respond_to?(:slug) ? team.slug : nil
team_label = if link_team && team_slug.present? team_label = if link_team && team_slug.present?
link_to(team.name, public_team_page_path(team_slug), class: "live-card__team-link") 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) def live_match_page_heading(match)
team = match.team 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 content_tag(:div, class: "live-page-heading") do
safe_join([ safe_join([
content_tag(:p, club_name, class: "live-page-heading__club"), content_tag(:p, club_name, class: "live-page-heading__club"),
@@ -79,7 +79,7 @@ module Public
end end
def live_score_points_label(match, score_state) def live_score_points_label(match, score_state)
return "" unless score_state return t("score.fallback") unless score_state
board = match.effective_board_type board = match.effective_board_type
home = case board home = case board
@@ -91,7 +91,7 @@ module Public
else score_state.away_points else score_state.away_points
end 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 end
private private
@@ -101,8 +101,11 @@ module Public
set_no = data[:set] || data["set"] set_no = data[:set] || data["set"]
home = data[:home] || data["home"] home = data[:home] || data["home"]
away = data[:away] || data["away"] away = data[:away] || data["away"]
label = set_no.present? ? "Set #{set_no}" : "Set" if set_no.present?
"#{label} #{home}-#{away}" 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 end
end end
+49 -3
View File
@@ -12,14 +12,60 @@ module Public
when "basket", "timed" when "basket", "timed"
live_score_period_label(match, score) live_score_period_label(match, score)
when "timer" when "timer"
"Cronometro" t("regia.board.stopwatch")
when "generic" when "generic"
"Punteggio" t("regia.board.generic_score")
else else
"Set #{score.current_set}" t("regia.board.set_label", number: score.current_set)
end end
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) def format_clock_secs(secs, count_up: false)
total = secs.to_i total = secs.to_i
return count_up ? "0:00" : "" if total <= 0 && !count_up return count_up ? "0:00" : "" if total <= 0 && !count_up
@@ -1,7 +1,7 @@
module Public module Public
module TeamPagesHelper module TeamPagesHelper
def team_page_meta_description(team, club) 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 end
def filter_params_for_canonical def filter_params_for_canonical
+2 -2
View File
@@ -19,14 +19,14 @@ module RosterHelper
def roster_category_options def roster_category_options
TeamRosterMember::DISPLAY_ORDER.map do |cat| TeamRosterMember::DISPLAY_ORDER.map do |cat|
[TeamRosterMember::CATEGORY_LABELS[cat], cat] [TeamRosterMember.category_label(cat), cat]
end end
end end
def roster_player_role_options(team, selected = nil) def roster_player_role_options(team, selected = nil)
roles = Sports::PlayerRoles.for_team(team) roles = Sports::PlayerRoles.for_team(team)
options_for_select( options_for_select(
[["— Seleziona ruolo —", ""]] + roles.map { |r| [r, r] }, [[t("roster.role_select_blank"), ""]] + roles.map { |r| [r, r] },
selected selected
) )
end end
@@ -4,15 +4,18 @@ module Billing
@invoice = params[:invoice] @invoice = params[:invoice]
@club = @invoice.club @club = @invoice.club
attachments["fattura-#{@invoice.number.parameterize}.pdf"] = { I18n.with_locale(I18n.locale) do
prefix = t("mailers.invoice.attachment_prefix")
attachments["#{prefix}-#{@invoice.number.parameterize}.pdf"] = {
mime_type: "application/pdf", mime_type: "application/pdf",
content: @invoice.pdf.download content: @invoice.pdf.download
} }
mail( mail(
to: @club.billing_email, to: @club.billing_email,
subject: "Fattura #{@invoice.display_number} — Match Live TV" subject: t("mailers.invoice.subject", number: @invoice.display_number)
) )
end end
end end
end end
end
@@ -9,12 +9,15 @@ module Recordings
@recipient = recipient @recipient = recipient
@replay_url = recording.replay_url @replay_url = recording.replay_url
@expires_at = recording.expires_at @expires_at = recording.expires_at
@archive_url = "#{MatchLiveTv.app_public_url.chomp('/')}/clubs/#{@club.id}/replays"
I18n.with_locale(I18n.locale) do
mail( mail(
to: recipient.email, to: recipient.email,
subject: "Replay pronto — #{recording.title_or_default}" subject: t("mailers.replay_ready.subject", title: recording.title_or_default)
) )
end end
end
def replay_expiring_soon(recording:, recipient:) def replay_expiring_soon(recording:, recipient:)
@recording = recording @recording = recording
@@ -24,11 +27,14 @@ module Recordings
@replay_url = recording.replay_url @replay_url = recording.replay_url
@expires_at = recording.expires_at @expires_at = recording.expires_at
@days_left = ((recording.expires_at - Time.current) / 1.day).ceil @days_left = ((recording.expires_at - Time.current) / 1.day).ceil
@archive_url = "#{MatchLiveTv.app_public_url.chomp('/')}/clubs/#{@club.id}/replays"
I18n.with_locale(I18n.locale) do
mail( mail(
to: recipient.email, to: recipient.email,
subject: "Replay in scadenza tra #{@days_left} giorni — #{recording.title_or_default}" subject: t("mailers.replay_expiring.subject", days: @days_left, title: recording.title_or_default)
) )
end end
end end
end end
end
+3 -1
View File
@@ -6,6 +6,8 @@ class UserMailer < ApplicationMailer
@reset_url = public_password_reset_url(token: token) @reset_url = public_password_reset_url(token: token)
@expiry_hours = MatchLiveTv.password_reset_expiry_hours @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
end end
@@ -1,14 +1,16 @@
module ClubBillingProfile module ClubBillingProfile
extend ActiveSupport::Concern extend ActiveSupport::Concern
BILLING_ENTITY_TYPES = { BILLING_ENTITY_TYPE_KEYS = %w[company individual nonprofit].freeze
"company" => "Società / ASD con P.IVA",
"individual" => "Persona fisica", class_methods do
"nonprofit" => "Associazione senza scopo di lucro" def billing_entity_types
}.freeze ClubBillingProfile::BILLING_ENTITY_TYPE_KEYS.index_with { |key| I18n.t("billing.entity_types.#{key}") }
end
end
included do 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_email, format: { with: URI::MailTo::EMAIL_REGEXP }, allow_blank: true
validates :billing_recipient_code, length: { is: 7 }, allow_blank: true validates :billing_recipient_code, length: { is: 7 }, allow_blank: true
validates :billing_province, length: { is: 2 }, 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). # Campi minimi per intestazione fattura e invio (email PDF + SDI/PEC).
def billing_profile_errors def billing_profile_errors
errors = [] errors = []
errors << "Tipo intestatario" if billing_entity_type.blank? errors << I18n.t("billing.profile_errors.entity_type") if billing_entity_type.blank?
errors << "Ragione sociale o nome intestatario" if billing_legal_name.blank? errors << I18n.t("billing.profile_errors.legal_name") if billing_legal_name.blank?
errors << "Email di fatturazione" if billing_email.blank? errors << I18n.t("billing.profile_errors.email") if billing_email.blank?
errors << "Indirizzo" if billing_address_line.blank? errors << I18n.t("billing.profile_errors.address") if billing_address_line.blank?
errors << "Città" if billing_city.blank? errors << I18n.t("billing.profile_errors.city") if billing_city.blank?
errors << "Provincia (sigla 2 lettere)" if billing_province.blank? || billing_province.to_s.length != 2 errors << I18n.t("billing.profile_errors.province") if billing_province.blank? || billing_province.to_s.length != 2
errors << "CAP" if billing_postal_code.blank? errors << I18n.t("billing.profile_errors.postal_code") if billing_postal_code.blank?
errors << "Paese (ISO)" if billing_country.blank? || billing_country.to_s.length != 2 errors << I18n.t("billing.profile_errors.country") if billing_country.blank? || billing_country.to_s.length != 2
errors.concat(billing_tax_id_errors) errors.concat(billing_tax_id_errors)
errors << "Codice destinatario SDI (7 caratteri) o PEC" if billing_recipient_code.blank? && billing_pec.blank? errors << I18n.t("billing.profile_errors.sdi_or_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_invalid") if billing_recipient_code.present? && billing_recipient_code.length != 7
errors errors
end end
def billing_tax_id_errors def billing_tax_id_errors
case billing_entity_type case billing_entity_type
when "company" when "company"
billing_vat_number.blank? ? ["Partita IVA"] : [] billing_vat_number.blank? ? [I18n.t("billing.profile_errors.vat_number")] : []
when "individual" when "individual"
billing_fiscal_code.blank? ? ["Codice Fiscale"] : [] billing_fiscal_code.blank? ? [I18n.t("billing.profile_errors.fiscal_code")] : []
else 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
end end
@@ -56,7 +58,7 @@ module ClubBillingProfile
# Righe per intestazione fattura in admin (etichetta, valore). # Righe per intestazione fattura in admin (etichetta, valore).
def billing_profile_invoice_lines def billing_profile_invoice_lines
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 << ["Intestatario", billing_legal_name]
lines << ["P.IVA", billing_vat_number] if billing_vat_number.present? lines << ["P.IVA", billing_vat_number] if billing_vat_number.present?
lines << ["Codice fiscale", billing_fiscal_code] if billing_fiscal_code.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 def public_status_label
active = active_stream_session active = active_stream_session
return "In corso / da riprendere" if active return I18n.t("matches.status.active") if active
return "Programmata" if scheduled_upcoming? return I18n.t("matches.status.scheduled") if scheduled_upcoming?
return "Pronta" if scheduled_at.present? return I18n.t("matches.status.ready") if scheduled_at.present?
"Senza orario" I18n.t("matches.status.no_time")
end end
scope :search_teams_or_opponents, lambda { |query| scope :search_teams_or_opponents, lambda { |query|
+6 -6
View File
@@ -164,13 +164,13 @@ class Recording < ApplicationRecord
end end
def status_label def status_label
return "Eliminato" if deleted? return I18n.t("recordings.status.deleted") if deleted?
return "Scaduto" if status == "expired" return I18n.t("recordings.status.expired") if status == "expired"
return "Errore" if status == "failed" return I18n.t("recordings.status.failed") if status == "failed"
return "In elaborazione" if status == "processing" return I18n.t("recordings.status.processing") if status == "processing"
return "Scade presto" if expires_at.present? && expires_at <= 7.days.from_now return I18n.t("recordings.status.expiring_soon") if expires_at.present? && expires_at <= 7.days.from_now
"Disponibile" I18n.t("recordings.status.available")
end end
private private
+12 -13
View File
@@ -2,12 +2,10 @@ class TeamRosterMember < ApplicationRecord
CATEGORIES = %w[staff coach manager player].freeze CATEGORIES = %w[staff coach manager player].freeze
# Ordine di visualizzazione in pagina organico # Ordine di visualizzazione in pagina organico
DISPLAY_ORDER = %w[coach manager player staff].freeze DISPLAY_ORDER = %w[coach manager player staff].freeze
CATEGORY_LABELS = {
"staff" => "Staff", def self.category_label(category)
"coach" => "Allenatori", I18n.t("roster.category.#{category}", default: category.to_s.capitalize)
"manager" => "Dirigenti", end
"player" => "Giocatori"
}.freeze
VOLLEYBALL_PLAYER_ROLES = Sports::PlayerRoles::BY_BOARD["volley"].freeze VOLLEYBALL_PLAYER_ROLES = Sports::PlayerRoles::BY_BOARD["volley"].freeze
@@ -27,7 +25,7 @@ class TeamRosterMember < ApplicationRecord
scope :by_category, ->(cat) { where(category: cat).ordered } scope :by_category, ->(cat) { where(category: cat).ordered }
def category_label def category_label
CATEGORY_LABELS[category] || category self.class.category_label(category)
end end
def initials def initials
@@ -58,10 +56,11 @@ class TeamRosterMember < ApplicationRecord
def default_role_label def default_role_label
case category case category
when "coach" then "Allenatore" when "coach" then I18n.t("roster.role.coach")
when "manager" then "Dirigente" when "manager" then I18n.t("roster.role.manager")
when "staff" then "Staff" when "staff" then I18n.t("roster.role.staff")
when "player" then jersey_number.present? ? "Giocatore ##{jersey_number}" : "Giocatore" when "player"
jersey_number.present? ? I18n.t("roster.role.player_numbered", number: jersey_number) : I18n.t("roster.role.player")
else category else category
end end
end end
@@ -70,12 +69,12 @@ class TeamRosterMember < ApplicationRecord
allowed = Sports::PlayerRoles.for_team(team) allowed = Sports::PlayerRoles.for_team(team)
return if role_label.in?(allowed) 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 end
def photo_file_type def photo_file_type
return if photo_file.content_type.in?(%w[image/png image/jpeg image/webp]) 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
end end
@@ -10,38 +10,37 @@ module Billing
if result.upgrade? if result.upgrade?
amount = format_amount(result.amount_cents, result.currency) amount = format_amount(result.amount_cents, result.currency)
if amount.present? 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 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 end
else else
when_label = result.effective_at.present? ? I18n.l(result.effective_at, format: :long) : "il prossimo rinnovo" when_label = result.effective_at.present? ? I18n.l(result.effective_at, format: :long) : I18n.t("billing.messages.next_renewal")
current_name = current_plan&.name || "il piano attuale" current_name = current_plan&.name || I18n.t("billing.messages.current_plan_generic")
"Passaggio a #{plan.name} (#{interval_label}) programmato: resti su #{current_name} fino al #{when_label}. " \ I18n.t("billing.messages.downgrade_scheduled", plan: plan.name, interval: interval_label, current_name: current_name, when: when_label)
"Nessun addebito aggiuntivo ora; dal prossimo ciclo pagherai il nuovo prezzo."
end end
end end
def button_label(plan:, interval:, kind:) def button_label(plan:, interval:, kind:)
price = PriceCatalog.label(plan_slug: plan.slug, interval: interval) price = PriceCatalog.label(plan_slug: plan.slug, interval: interval)
if kind == :checkout if kind == :checkout
"Attiva #{plan.name}#{price}" I18n.t("billing.messages.activate_button", plan: plan.name, price: price)
else else
"Passa a #{plan.name}#{price}" I18n.t("billing.messages.switch_button", plan: plan.name, price: price)
end end
end end
def billing_info_lines(subscription: nil) def billing_info_lines(subscription: nil)
lines = [ lines = [
"Upgrade (piano superiore o passaggio alla fatturazione annuale): la differenza viene addebitata subito sulla carta salvata in Stripe.", I18n.t("billing.messages.upgrade_info_line"),
"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.downgrade_info_line")
] ]
if subscription&.plan_change_pending? && subscription.pending_plan.present? if subscription&.plan_change_pending? && subscription.pending_plan.present?
when_at = subscription.current_period_end 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( 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 end
+5 -5
View File
@@ -1,26 +1,26 @@
<div style="max-width:360px;margin:4rem auto"> <div style="max-width:360px;margin:4rem auto">
<h2>Accesso admin</h2> <h2><%= t("admin.auth.new.title") %></h2>
<% if flash[:alert] %> <% if flash[:alert] %>
<p style="color:#ff6b6b"><%= flash[:alert] %></p> <p style="color:#ff6b6b"><%= flash[:alert] %></p>
<% end %> <% end %>
<%= form_with url: admin_login_path, method: :post, local: true do %> <%= form_with url: admin_login_path, method: :post, local: true do %>
<p> <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" <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"> style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p> </p>
<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" <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"> style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p> </p>
<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"> <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> </button>
</p> </p>
<% end %> <% end %>
<p style="color:#888;font-size:0.85rem;margin-top:1.5rem"> <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> </p>
</div> </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"> <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> </p>
<% if @clubs.many? %> <% if @clubs.many? %>
<p style="margin-bottom:16px"> <p style="margin-bottom:16px">
Filtra società: <%= t("admin.billing.index.filter_label") %>
<%= link_to "Tutte", admin_billing_path, class: (@filter_club ? nil : "admin-nav-active") %> <%= link_to t("admin.billing.index.filter_all"), admin_billing_path, class: (@filter_club ? nil : "admin-nav-active") %>
<% @clubs.each do |club| %> <% @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) %> · <%= link_to club.name, admin_billing_path(club_id: club.id), class: (@filter_club&.id == club.id ? "admin-nav-active" : nil) %>
<% end %> <% end %>
@@ -33,7 +33,7 @@
· <strong><%= payment.formatted_amount %></strong> · <strong><%= payment.formatted_amount %></strong>
</div> </div>
<% unless club.billing_profile_complete? %> <% 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 %> <% end %>
</header> </header>
@@ -46,16 +46,16 @@
<% end %> <% end %>
</dl> </dl>
<% else %> <% 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 %> <% end %>
<%= form_with url: admin_billing_payment_attach_pdf_path(payment), <%= form_with url: admin_billing_payment_attach_pdf_path(payment),
method: :post, multipart: true, local: true, class: "billing-upload-form" do %> method: :post, multipart: true, local: true, class: "billing-upload-form" do %>
<label class="billing-upload-form__label"> <label class="billing-upload-form__label">
PDF fattura <%= t("admin.billing.index.pdf_label") %>
<%= file_field_tag :pdf, accept: "application/pdf", required: true %> <%= file_field_tag :pdf, accept: "application/pdf", required: true %>
</label> </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 %> <% end %>
</div> </div>
</article> </article>
@@ -63,21 +63,21 @@
</div> </div>
<% else %> <% else %>
<p class="admin-flash" style="background:#1b3d1b;border-color:#2e7d32"> <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> </p>
<% end %> <% end %>
<% if @completed_payments.any? %> <% 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"> <table class="admin-table">
<thead> <thead>
<tr> <tr>
<th>Data</th> <th><%= t("admin.billing.index.table.date") %></th>
<th>Società</th> <th><%= t("admin.billing.index.table.club") %></th>
<th>Descrizione</th> <th><%= t("admin.billing.index.table.description") %></th>
<th>Importo</th> <th><%= t("admin.billing.index.table.amount") %></th>
<th>Fattura</th> <th><%= t("admin.billing.index.table.invoice") %></th>
<th>Stato</th> <th><%= t("admin.billing.index.table.status") %></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -96,4 +96,4 @@
</table> </table>
<% end %> <% 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"> <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? %> <% 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 %> <% end %>
</p> </p>
<div class="card"> <div class="card">
<%= form_with model: @invoice, url: admin_club_billing_invoice_path(@club, @invoice), multipart: true, method: :patch do |f| %> <%= 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.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]) %> <%= 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]", <%= number_field_tag "billing_invoice[amount_euros]",
@invoice.amount_cents / 100.0, @invoice.amount_cents / 100.0,
step: 0.01, min: 0.01, required: true, disabled: @invoice.status.in?(%w[sent]) %> step: 0.01, min: 0.01, required: true, disabled: @invoice.status.in?(%w[sent]) %>
<% if @payment %> <% 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 %> <% end %>
<%= f.label :pdf, "PDF fattura" %> <%= f.label :pdf, t("admin.billing_invoices.edit.pdf_label") %>
<% if @invoice.pdf.attached? %> <% 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 %> <% end %>
<%= f.file_field :pdf, accept: "application/pdf" %> <%= 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 %> <%= f.text_area :notes, rows: 2 %>
<div style="margin-top:16px;display:flex;gap:10px;flex-wrap:wrap"> <div style="margin-top:16px;display:flex;gap:10px;flex-wrap:wrap">
<% unless @invoice.status.in?(%w[sent]) %> <% unless @invoice.status.in?(%w[sent]) %>
<%= f.submit "Salva bozza", class: "btn btn-secondary" %> <%= f.submit t("admin.billing_invoices.edit.save_draft"), name: "commit_action", value: "draft", class: "btn btn-secondary" %>
<%= f.submit "Emetti e invia via email", class: "btn btn-primary", <%= f.submit t("admin.billing_invoices.edit.issue_send"), name: "commit_action", value: "issue", class: "btn btn-primary",
data: { turbo_confirm: "Inviare la fattura a #{@club.billing_email}?" } %> data: { turbo_confirm: t("admin.billing_invoices.edit.issue_confirm", email: @club.billing_email) } %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
</div> </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> <p>
<%= link_to "← Dashboard", admin_root_path %> <%= link_to t("admin.billing_invoices.index.back_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.new_invoice"), new_admin_club_billing_invoice_path(@club), class: "btn btn-secondary" %>
</p> </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? %> <% if @payments.any? %>
<table class="data"> <table class="data">
<thead> <thead>
<tr> <tr>
<th>Data</th> <th><%= t("admin.billing_invoices.index.table.date") %></th>
<th>Descrizione</th> <th><%= t("admin.billing_invoices.index.table.description") %></th>
<th>Importo</th> <th><%= t("admin.billing_invoices.index.table.amount") %></th>
<th>Fattura</th> <th><%= t("admin.billing_invoices.index.table.invoice") %></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -26,16 +26,16 @@
<td> <td>
<% if inv %> <% if inv %>
<%= inv.number %><%= inv.status %> <%= inv.number %><%= inv.status %>
<% if inv.pdf.attached? %> (PDF)<% end %> <% if inv.pdf.attached? %><%= t("admin.billing_invoices.index.pdf_suffix") %><% end %>
<% else %> <% else %>
<%= t("admin.common.dash") %>
<% end %> <% end %>
</td> </td>
<td> <td>
<% if inv %> <% 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 %> <% 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 %> <% end %>
</td> </td>
</tr> </tr>
@@ -43,14 +43,21 @@
</tbody> </tbody>
</table> </table>
<% else %> <% else %>
<p>Nessun pagamento registrato per questa società.</p> <p><%= t("admin.billing_invoices.index.no_payments") %></p>
<% end %> <% 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? %> <% if @invoices.any? %>
<table class="data"> <table class="data">
<thead> <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> </thead>
<tbody> <tbody>
<% @invoices.each do |inv| %> <% @invoices.each do |inv| %>
@@ -59,12 +66,12 @@
<td><%= inv.issued_on %></td> <td><%= inv.issued_on %></td>
<td><%= inv.formatted_amount %></td> <td><%= inv.formatted_amount %></td>
<td><%= inv.status %></td> <td><%= inv.status %></td>
<td><%= inv.billing_payment_id.present? ? "Sì" : "No" %></td> <td><%= inv.billing_payment_id.present? ? t("admin.common.yes") : t("admin.common.no") %></td>
<td><%= link_to "Modifica", edit_admin_club_billing_invoice_path(@club, inv) %></td> <td><%= link_to t("admin.billing_invoices.index.edit"), edit_admin_club_billing_invoice_path(@club, inv) %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<% else %> <% else %>
<p>Nessuna fattura.</p> <p><%= t("admin.billing_invoices.index.no_invoices") %></p>
<% end %> <% end %>
@@ -1,32 +1,32 @@
<h1>Nuova fattura — <%= @club.name %></h1> <h1><%= t("admin.billing_invoices.new.title", club: @club.name) %></h1>
<% if @payment %> <% 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 %> <% end %>
<div class="card"> <div class="card">
<%= form_with model: @invoice, url: admin_club_billing_invoices_path(@club) do |f| %> <%= form_with model: @invoice, url: admin_club_billing_invoices_path(@club) do |f| %>
<%= f.hidden_field :billing_payment_id if @payment %> <%= 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.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 %> <%= 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]", <%= number_field_tag "billing_invoice[amount_euros]",
(@invoice.amount_cents ? @invoice.amount_cents / 100.0 : nil), (@invoice.amount_cents ? @invoice.amount_cents / 100.0 : nil),
step: 0.01, min: 0.01, required: true %> 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 %> <%= f.text_area :notes, rows: 2 %>
<p style="color:#666;font-size:0.9rem;margin-top:12px"> <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> </p>
<%= f.submit "Crea bozza fattura", class: "btn btn-primary" %> <%= f.submit t("admin.billing_invoices.new.submit"), class: "btn btn-primary" %>
<% end %> <% end %>
</div> </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 %> <% content_for :replay_archive_styles, true %>
<%= render "recordings/club_archive", admin_mode: true %> <%= render "recordings/club_archive", admin_mode: true %>
@@ -1,43 +1,43 @@
<%# locals: (club:, subscription:, return_to: nil) %> <%# 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"> <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"> <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> </p>
<% sub = subscription %> <% sub = subscription %>
<% if sub&.admin_comped? %> <% if sub&.admin_comped? %>
<p style="margin:0 0 12px"> <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? %> <% if sub.admin_comped_reason.present? %>
· <%= sub.admin_comped_reason %> · <%= sub.admin_comped_reason %>
<% end %> <% end %>
<% if sub.admin_comped_at.present? %> <% if sub.admin_comped_at.present? %>
<br><span style="color:#888;font-size:0.85rem">Dal <%= l(sub.admin_comped_at, format: :long) %> <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? %> · admin <%= sub.admin_comped_by.username %><% end %> <% if sub.admin_comped_by.present? %> <%= t("admin.comped.by_admin", username: sub.admin_comped_by.username) %><% end %>
</span> </span>
<% end %> <% end %>
<% if sub.stripe_subscription_id.present? %> <% 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 %> <% end %>
</p> </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), revoke_comped_admin_club_path(club, return_to: return_to),
method: :delete, method: :delete,
class: "admin-btn admin-btn--secondary", 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 %> <% else %>
<p style="margin:0 0 12px;color:#888"> <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? %> <% if sub&.stripe_subscription_id.present? %>
· pagamento Stripe attivo <%= t("admin.comped.stripe_active") %>
<% end %> <% end %>
</p> </p>
<%= form_with url: grant_comped_admin_club_path(club), method: :post, local: true, class: "admin-comped-form" do %> <%= 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? %> <%= hidden_field_tag :return_to, return_to if return_to.present? %>
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end"> <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"> <label style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem">
Piano <%= t("admin.comped.plan_label") %>
<%= select_tag :plan_slug, <%= select_tag :plan_slug,
options_for_select( options_for_select(
[["Premium Light", "premium_light"], ["Premium Full", "premium_full"]] [["Premium Light", "premium_light"], ["Premium Full", "premium_full"]]
@@ -46,10 +46,10 @@
class: "admin-input" %> class: "admin-input" %>
</label> </label>
<label style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;flex:1;min-width:200px"> <label style="display:flex;flex-direction:column;gap:4px;font-size:0.85rem;flex:1;min-width:200px">
Motivo (es. sponsor 2026) <%= t("admin.comped.reason_label") %>
<%= text_field_tag :reason, nil, placeholder: "Sponsor, promozione…", class: "admin-input", style: "width:100%" %> <%= text_field_tag :reason, nil, placeholder: t("admin.comped.reason_placeholder"), class: "admin-input", style: "width:100%" %>
</label> </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> </div>
<% end %> <% end %>
<% 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"> <p style="color:#888;margin-bottom:16px">
Abbonamenti omaggio (sponsor / promozioni) e stato YouTube per squadra. <%= t("admin.clubs.index.subtitle_html", billing_link: link_to(t("admin.clubs.index.billing_link"), admin_billing_path)) %>
Pagamenti Stripe: <%= link_to "Fatturazione", admin_billing_path %>.
</p> </p>
<table class="admin-table"> <table class="admin-table">
<thead> <thead>
<tr> <tr>
<th>Società</th> <th><%= t("admin.clubs.index.table.club") %></th>
<th>Piano</th> <th><%= t("admin.clubs.index.table.plan") %></th>
<th>Squadre</th> <th><%= t("admin.clubs.index.table.teams") %></th>
<th>Omaggio</th> <th><%= t("admin.clubs.index.table.comped") %></th>
<th>Stripe</th> <th><%= t("admin.clubs.index.table.stripe") %></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -20,20 +19,20 @@
<% sub = club.subscription %> <% sub = club.subscription %>
<tr> <tr>
<td><strong><%= club.name %></strong></td> <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><%= club.teams.size %></td>
<td> <td>
<% if sub&.admin_comped? %> <% 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 %> <% if sub.admin_comped_reason.present? %> · <%= sub.admin_comped_reason %><% end %>
<% else %> <% else %>
<%= t("admin.common.dash") %>
<% end %> <% end %>
</td> </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> <td>
<%= link_to "Gestisci", admin_club_path(club) %> <%= link_to t("admin.clubs.index.manage"), admin_club_path(club) %>
· <%= link_to "Fatture", admin_billing_path(club_id: club.id) %> · <%= link_to t("admin.clubs.index.invoices"), admin_billing_path(club_id: club.id) %>
</td> </td>
</tr> </tr>
<% end %> <% 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> <h2><%= @club.name %></h2>
<p style="color:#888"> <p style="color:#888">
Sport: <%= @club.sport %> <%= t("admin.clubs.show.sport_label", sport: @club.sport) %>
· <%= link_to "Archivio replay", admin_club_recordings_path(@club) %> · <%= link_to t("admin.clubs.show.recordings_archive"), admin_club_recordings_path(@club) %>
· <%= link_to "Pagamenti e fatture", admin_billing_path(club_id: @club.id) %> · <%= link_to t("admin.clubs.show.billing_link"), admin_billing_path(club_id: @club.id) %>
· <%= link_to "Canale YouTube piattaforma", admin_youtube_platform_path %> · <%= link_to t("admin.clubs.show.youtube_platform_link"), admin_youtube_platform_path %>
</p> </p>
<%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %> <%= render "admin/clubs/comped_form", club: @club, subscription: @subscription, return_to: admin_club_path(@club) %>
<% cred = @club.youtube_credential %> <% cred = @club.youtube_credential %>
<p style="margin-top:16px"> <p style="margin-top:16px">
<strong>YouTube società:</strong> <strong><%= t("admin.clubs.show.youtube_club_label") %></strong>
<% if cred %> <% 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? } %> <% 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 %> <% else %>
<span class="muted"></span> <span class="muted"><%= t("admin.common.dash") %></span>
<% end %> <% end %>
</p> </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? %> <% if @teams.empty? %>
<p class="muted">Nessuna squadra registrata.</p> <p class="muted"><%= t("admin.clubs.show.no_teams") %></p>
<% else %> <% else %>
<table class="admin-table"> <table class="admin-table">
<thead> <thead>
<tr> <tr>
<th>Squadra</th> <th><%= t("admin.clubs.show.table.team") %></th>
<th>Sport</th> <th><%= t("admin.clubs.show.table.sport") %></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -39,7 +39,7 @@
<tr> <tr>
<td><strong><%= team.name %></strong></td> <td><strong><%= team.name %></strong></td>
<td><%= team.sport %></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> </tr>
<% end %> <% end %>
</tbody> </tbody>
@@ -2,147 +2,153 @@
<section class="kpi-grid"> <section class="kpi-grid">
<div class="kpi-card kpi-card--accent"> <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-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>
<div class="kpi-card"> <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-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>
<div class="kpi-card"> <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-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>
<div class="kpi-card"> <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-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>
<div class="kpi-card"> <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-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>
<div class="kpi-card"> <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-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>
<div class="kpi-card"> <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-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>
<div class="kpi-card"> <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-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> </div>
</section> </section>
<section class="charts-grid"> <section class="charts-grid">
<div class="chart-card"> <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 class="chart-wrap"><canvas id="chart-cpu"></canvas></div>
</div> </div>
<div class="chart-card"> <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 class="chart-wrap"><canvas id="chart-mem"></canvas></div>
</div> </div>
</section> </section>
<section class="panel" style="margin-bottom:1.25rem"> <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 %> <% critical = Ops::Incident.critical_open.count %>
<% warnings = Ops::Incident.open.where(severity: "warning").count %> <% warnings = Ops::Incident.open.where(severity: "warning").count %>
<% if critical.positive? %> <% 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? %> <% 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 %> <% 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 %> <% end %>
</section> </section>
<section class="charts-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));"> <section class="charts-grid" style="grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));">
<div class="chart-card"> <div class="chart-card">
<h3>Disco sistema</h3> <h3><%= t("admin.dashboard.disk.system_title") %></h3>
<% root = @host[:disk][:root] %> <% root = @host[:disk][:root] %>
<% if root[:total_bytes] %> <% if root[:total_bytes] %>
<div class="disk-row"> <div class="disk-row">
<header><span><%= root[:path] %></span><span><%= format_bytes(root[:used_bytes]) %> / <%= format_bytes(root[:total_bytes]) %></span></header> <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> <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> </div>
<% else %> <% else %>
<p class="empty">Metriche disco non disponibili</p> <p class="empty"><%= t("admin.dashboard.disk.not_available") %></p>
<% end %> <% end %>
</div> </div>
<div class="chart-card"> <div class="chart-card">
<h3>Archivio registrazioni</h3> <h3><%= t("admin.dashboard.disk.recordings_title") %></h3>
<% rec = @host[:disk][:recordings] %> <% rec = @host[:disk][:recordings] %>
<% if rec[:total_bytes] %> <% if rec[:total_bytes] %>
<div class="disk-row"> <div class="disk-row">
<header><span><%= rec[:path] %></span><span><%= format_bytes(rec[:used_bytes]) %> / <%= format_bytes(rec[:total_bytes]) %></span></header> <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> <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] %> <% 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 %> <% end %>
</div> </div>
<% else %> <% 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 %> <% end %>
</div> </div>
</section> </section>
<section class="admin-panels"> <section class="admin-panels">
<div class="panel"> <div class="panel">
<h2>Sessioni attive</h2> <h2><%= t("admin.dashboard.sessions.title") %></h2>
<% if @active_sessions.any? %> <% if @active_sessions.any? %>
<table class="admin-table"> <table class="admin-table">
<thead> <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> </thead>
<tbody id="active-sessions-body"> <tbody id="active-sessions-body">
<% @active_sessions.each do |s| %> <% @active_sessions.each do |s| %>
<tr> <tr>
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td> <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><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"> <td class="admin-link-compact">
<% admin_session_watch_links(s).each do |link| %> <% admin_session_watch_links(s).each do |link| %>
<%= link_to link[:label], link[:url], target: "_blank", rel: "noopener", class: "admin-link-chip" %> <%= link_to link[:label], link[:url], target: "_blank", rel: "noopener", class: "admin-link-chip" %>
<% end %> <% 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>
<td class="admin-actions"> <td class="admin-actions">
<%= link_to "Dettaglio", admin_session_path(s) %> <%= link_to t("admin.dashboard.sessions.detail"), 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?" } } %> <%= 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> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<% else %> <% else %>
<p class="empty">Nessuna sessione attiva in questo momento.</p> <p class="empty"><%= t("admin.dashboard.sessions.none") %></p>
<% end %> <% end %>
</div> </div>
<div class="panel"> <div class="panel">
<h2>Squadre</h2> <h2><%= t("admin.dashboard.teams.title") %></h2>
<ul class="team-list"> <ul class="team-list">
<% @teams.each do |t| %> <% @teams.each do |t_| %>
<li> <li>
<%= link_to t.name, admin_team_path(t) %> <%= link_to t_.name, admin_team_path(t_) %>
<span class="muted"><%= t.matches.count %> partite</span> <span class="muted"><%= t("admin.dashboard.teams.matches_count", count: t_.matches.count) %></span>
</li> </li>
<% end %> <% end %>
</ul> </ul>
<% if @stats[:teams_count] > @teams.size %> <% 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 %> <% end %>
</div> </div>
</section> </section>
+22 -16
View File
@@ -2,30 +2,30 @@
<section class="kpi-grid"> <section class="kpi-grid">
<div class="kpi-card <%= @summary[:open_critical].positive? ? 'kpi-card--danger' : 'kpi-card--accent' %>"> <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 class="kpi-value"><%= @summary[:open_critical] %></div>
</div> </div>
<div class="kpi-card"> <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 class="kpi-value"><%= @summary[:open_warning] %></div>
</div> </div>
<div class="kpi-card"> <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 class="kpi-value"><%= @summary[:open_total] %></div>
</div> </div>
</section> </section>
<div class="panel" style="margin-bottom:1.5rem"> <div class="panel" style="margin-bottom:1.5rem">
<h2>Incidenti aperti</h2> <h2><%= t("admin.ops.open.title") %></h2>
<% if @open_incidents.any? %> <% if @open_incidents.any? %>
<table class="admin-table"> <table class="admin-table">
<thead> <thead>
<tr> <tr>
<th>Severità</th> <th><%= t("admin.ops.open.table.severity") %></th>
<th>Tipo</th> <th><%= t("admin.ops.open.table.kind") %></th>
<th>Titolo</th> <th><%= t("admin.ops.open.table.title") %></th>
<th>Occ.</th> <th><%= t("admin.ops.open.table.occurrences") %></th>
<th>Ultimo</th> <th><%= t("admin.ops.open.table.last_seen") %></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -44,26 +44,32 @@
<td class="muted"><%= inc.last_seen_at&.strftime("%d/%m %H:%M") %></td> <td class="muted"><%= inc.last_seen_at&.strftime("%d/%m %H:%M") %></td>
<td class="admin-actions"> <td class="admin-actions">
<% unless inc.status == 'acknowledged' %> <% 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 %> <% end %>
<%= button_to "Risolvi", resolve_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm" %> <%= button_to t("admin.ops.actions.resolve"), 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.mute"), mute_admin_op_path(inc), method: :post, class: "admin-btn admin-btn--sm admin-btn--outline" %>
</td> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<% else %> <% else %>
<p class="empty">Nessun incidente aperto. La piattaforma risulta sana.</p> <p class="empty"><%= t("admin.ops.open.none") %></p>
<% end %> <% end %>
</div> </div>
<div class="panel"> <div class="panel">
<h2>Risolti (ultimi 30 giorni)</h2> <h2><%= t("admin.ops.resolved.title") %></h2>
<% if @resolved_incidents.any? %> <% if @resolved_incidents.any? %>
<table class="admin-table"> <table class="admin-table">
<thead> <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> </thead>
<tbody> <tbody>
<% @resolved_incidents.each do |inc| %> <% @resolved_incidents.each do |inc| %>
@@ -78,6 +84,6 @@
</tbody> </tbody>
</table> </table>
<% else %> <% else %>
<p class="empty">Nessun incidente risolto di recente.</p> <p class="empty"><%= t("admin.ops.resolved.none") %></p>
<% end %> <% end %>
</div> </div>
@@ -1,29 +1,29 @@
<div style="max-width:400px"> <div style="max-width:400px">
<h2>Cambia password</h2> <h2><%= t("admin.passwords.edit.title") %></h2>
<% if flash[:alert] %> <% if flash[:alert] %>
<p style="color:#ff6b6b"><%= flash[:alert] %></p> <p style="color:#ff6b6b"><%= flash[:alert] %></p>
<% end %> <% end %>
<%= form_with url: admin_password_path, method: :patch, local: true do %> <%= form_with url: admin_password_path, method: :patch, local: true do %>
<p> <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" <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"> style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p> </p>
<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" <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"> style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p> </p>
<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" <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"> style="width:100%;padding:0.5rem;margin-top:0.25rem;background:#1E1E1E;border:1px solid #333;color:#fff;border-radius:6px">
</p> </p>
<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"> <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> </button>
<%= link_to "Annulla", admin_root_path %> <%= link_to t("admin.passwords.edit.cancel"), admin_root_path %>
</p> </p>
<% end %> <% end %>
</div> </div>
@@ -1,6 +1,6 @@
<% watch_links = admin_session_watch_links(session) %> <% watch_links = admin_session_watch_links(session) %>
<section class="admin-links-panel"> <section class="admin-links-panel">
<h3>Link diretta</h3> <h3><%= t("admin.sessions.links.watch_title") %></h3>
<% if watch_links.any? %> <% if watch_links.any? %>
<ul class="admin-link-list"> <ul class="admin-link-list">
<% watch_links.each do |link| %> <% watch_links.each do |link| %>
@@ -11,28 +11,28 @@
<% end %> <% end %>
</ul> </ul>
<% else %> <% else %>
<p class="muted">Nessun link video disponibile per questa sessione.</p> <p class="muted"><%= t("admin.sessions.links.none") %></p>
<% end %> <% end %>
<h3>Link regia</h3> <h3><%= t("admin.sessions.links.regia_title") %></h3>
<% if session.terminal? %> <% if session.terminal? %>
<p class="muted">Sessione terminata — link regia non disponibile.</p> <p class="muted"><%= t("admin.sessions.links.terminated") %></p>
<% else %> <% else %>
<% if flash[:regia_url].present? %> <% if flash[:regia_url].present? %>
<p class="admin-link-generated"> <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" %> <%= link_to flash[:regia_url], flash[:regia_url], target: "_blank", rel: "noopener", class: "admin-link-url" %>
</p> </p>
<p><code class="admin-link-code"><%= flash[:regia_url] %></code></p> <p><code class="admin-link-code"><%= flash[:regia_url] %></code></p>
<% if (expires = admin_regia_expires_label(flash[:regia_expires_at])) %> <% 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 %> <% end %>
<% elsif session.regia_token_active? %> <% 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 %> <% 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 %> <% end %>
<%= button_to "Genera link regia", <%= button_to t("admin.sessions.links.generate_button"),
regia_link_admin_session_path(session), regia_link_admin_session_path(session),
method: :post, method: :post,
class: "admin-btn admin-btn--secondary admin-btn--sm", 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"> <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> <tbody>
<% @sessions.each do |s| %> <% @sessions.each do |s| %>
<tr> <tr>
@@ -11,9 +19,9 @@
<% admin_session_watch_links(s).each do |link| %> <% admin_session_watch_links(s).each do |link| %>
<%= link_to link[:label], link[:url], target: "_blank", rel: "noopener", class: "admin-link-chip" %> <%= link_to link[:label], link[:url], target: "_blank", rel: "noopener", class: "admin-link-chip" %>
<% end %> <% 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>
<td><%= link_to "Dettaglio", admin_session_path(s) %></td> <td><%= link_to t("admin.sessions.index.detail"), admin_session_path(s) %></td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
+15 -9
View File
@@ -1,26 +1,32 @@
<h2>Session <%= @session.id %></h2> <h2><%= t("admin.sessions.show.title", id: @session.id) %></h2>
<p>Status: <strong><%= @session.status %></strong></p> <p><%= t("admin.sessions.show.status_label") %> <strong><%= @session.status %></strong></p>
<% unless @session.terminal? %> <% unless @session.terminal? %>
<p> <p>
<%= button_to "Termina sessione", <%= button_to t("admin.sessions.show.stop_button"),
stop_admin_session_path(@session), stop_admin_session_path(@session),
method: :post, method: :post,
class: "admin-btn admin-btn--danger", 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> </p>
<% end %> <% 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 %> <%= render "admin/sessions/links", session: @session %>
<% if @session.youtube_broadcast_id %> <% 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 %> <% 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> <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> <tbody>
<% @events.each do |e| %> <% @events.each do |e| %>
<tr> <tr>
+13 -11
View File
@@ -1,34 +1,36 @@
<% if @team.club %> <% 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 %> <% end %>
<h2><%= @team.name %></h2> <h2><%= @team.name %></h2>
<p>Sport: <%= @team.sport %></p> <p><%= t("admin.teams.show.sport_label", sport: @team.sport) %></p>
<% if @team.club %> <% 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 %> <% end %>
<% yt = Youtube::TeamStatus.new(@team) %> <% yt = Youtube::TeamStatus.new(@team) %>
<p> <p>
YouTube: <%= t("admin.teams.show.youtube_label") %>
<% if yt.selectable? %> <% if yt.selectable? %>
<strong><%= yt.channel_title || "—" %></strong> <strong><%= yt.channel_title || t("admin.common.dash") %></strong>
<% if yt.uses_platform_channel? %> <% 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 %> <% else %>
(canale società) <%= t("admin.teams.show.youtube_club_channel") %>
<% end %> <% end %>
<% else %> <% else %>
non pronto <%= t("admin.teams.show.youtube_not_ready") %>
<% end %> <% end %>
</p> </p>
<% if @team.entitlements.premium_full? && @team.club.youtube_credential.blank? %> <% if @team.entitlements.premium_full? && @team.club.youtube_credential.blank? %>
<p class="muted"> <p class="muted">
Senza canale collegato, lapp usa il canale Match Live TV. <%= t("admin.teams.show.no_channel_hint_html", link: link_to(t("admin.teams.show.link_channel"), public_club_path(@team.club))) %>
<%= link_to "Collega canale (pagina società)", public_club_path(@team.club) %>.
</p> </p>
<% end %> <% end %>
<h3>Partite</h3> <h3><%= t("admin.teams.show.matches_title") %></h3>
<ul> <ul>
<% @matches.each do |m| %> <% @matches.each do |m| %>
<li><%= m.opponent_name %><%= m.scheduled_at %></li> <li><%= m.opponent_name %><%= m.scheduled_at %></li>
@@ -1,10 +1,10 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="it"> <html lang="<%= I18n.locale %>">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<title>Token YouTube — Match Live TV</title> <title><%= t("admin.youtube.platform_token.page_title") %></title>
<style> <style>
body { font-family: system-ui, sans-serif; background: #0a0a0e; color: #f0f0f5; margin: 0; padding: 32px 20px; line-height: 1.5; } 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; } .wrap { max-width: 720px; margin: 0 auto; }
@@ -17,27 +17,24 @@
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
<h1>Token canale Match Live TV</h1> <h1><%= t("admin.youtube.platform_token.title") %></h1>
<% if @channel_title.present? %> <% if @channel_title.present? %>
<p>Canale: <strong><%= @channel_title %></strong></p> <p><%= t("admin.youtube.platform_token.channel_label", title: @channel_title) %></p>
<% end %> <% end %>
<% if @refresh_token.present? %> <% if @refresh_token.present? %>
<p class="ok">Collegamento riuscito. Copia subito questa riga:</p> <p class="ok"><%= t("admin.youtube.platform_token.success") %></p>
<p>Aggiungi in <code>/opt/matchlivetv/infra/.env</code>:</p> <p><%= t("admin.youtube.platform_token.env_hint_html") %></p>
<pre id="yt-token">YOUTUBE_PLATFORM_REFRESH_TOKEN=<%= @refresh_token %></pre> <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 %> <% else %>
<p class="muted"> <p class="muted">
Google non ha restituito un refresh token. In <%= t("admin.youtube.platform_token.no_refresh_token_html") %>
<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>.
</p> </p>
<% end %> <% end %>
<p><a href="/admin/">← Dashboard admin</a></p> <p><a href="/admin/"><%= t("admin.youtube.platform_token.back_dashboard") %></a></p>
</div> </div>
</body> </body>
</html> </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) %> <p><%= raw t("mailers.invoice.body_html",
per un importo di <strong><%= @invoice.formatted_amount %></strong>, relativa alla società <strong><%= @club.billing_legal_name.presence || @club.name %></strong>.</p> 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) %> <%= t("mailers.invoice.body_text",
(<%= @invoice.formatted_amount %>) per <%= @club.billing_legal_name.presence || @club.name %>. 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, <%= t("mailers.invoice.thanks") %>
Match Live TV <%= t("mailers.invoice.signoff") %>
+11 -10
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="<%= current_locale %>">
<head> <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="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow"> <meta name="robots" content="noindex, nofollow">
<link rel="stylesheet" href="/admin.css?v=2"> <link rel="stylesheet" href="/admin.css?v=2">
@@ -20,18 +20,19 @@
<h1>MATCH <span>LIVE</span> TV — Admin</h1> <h1>MATCH <span>LIVE</span> TV — Admin</h1>
<nav class="admin-nav"> <nav class="admin-nav">
<% if admin_logged_in? %> <% 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 %> <% ops_critical = Ops::Incident.critical_open.count %>
<%= link_to admin_ops_path, class: ("active" if controller_name == "ops") do %> <%= 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 %> <% end %>
<%= link_to "Società e squadre", admin_clubs_path, class: ("active" if controller_name.in?(%w[clubs teams club_recordings])) %> <%= link_to t("admin.layout.nav.clubs"), 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 t("admin.layout.nav.billing"), 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 t("admin.layout.nav.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 t("admin.layout.nav.sessions"), admin_sessions_path, class: ("active" if controller_name == "sessions") %>
<%= link_to "Password", edit_admin_password_path %> <%= link_to t("admin.layout.nav.password"), edit_admin_password_path %>
<%= button_to "Esci", admin_logout_path, method: :delete %> <%= button_to t("admin.layout.nav.logout"), admin_logout_path, method: :delete %>
<% end %> <% end %>
<%= render "shared/language_switcher", locale_path: admin_locale_path %>
</nav> </nav>
</header> </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="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"> <link rel="stylesheet" href="/marketing.css?v=44">
</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/cookie_banner" %>
<%= render "shared/marketing_nav" %> <%= render "shared/marketing_nav" %>
<main> <main>
@@ -10,7 +10,7 @@
<link rel="stylesheet" href="/live.css?v=26"> <link rel="stylesheet" href="/live.css?v=26">
<%= yield :head %> <%= yield :head %>
</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/cookie_banner" %>
<%= render "shared/marketing_nav" %> <%= render "shared/marketing_nav" %>
<main class="live-main"> <main class="live-main">
+7 -7
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="it"> <html lang="<%= html_lang %>">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
@@ -34,17 +34,17 @@
<div class="wrap"> <div class="wrap">
<%= link_to public_pricing_path, class: "brand" do %>Match <span>Live TV</span><% end %> <%= link_to public_pricing_path, class: "brand" do %>Match <span>Live TV</span><% end %>
<nav> <nav>
<%= link_to "Prezzi", public_pricing_path %> <%= link_to t("nav.pricing"), public_pricing_path %>
<% if logged_in? %> <% if logged_in? %>
<% if current_user.primary_club %> <% 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? %> <% 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 %> <% 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 %> <% else %>
· <%= link_to "Accedi", public_login_path %> · <%= link_to t("nav.login"), 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.signup"), public_signup_path, class: "btn btn-primary", style: "padding:6px 12px;font-size:0.85rem" %>
<% end %> <% end %>
</nav> </nav>
</div> </div>
+2 -2
View File
@@ -1,11 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="it"> <html lang="<%= html_lang %>">
<head> <head>
<meta charset="utf-8"> <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="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="apple-mobile-web-app-capable" content="yes">
<meta name="theme-color" content="#0a0a0e"> <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" %> <%= render "shared/meta_tags" %>
<link rel="stylesheet" href="/regia.css?v=1"> <link rel="stylesheet" href="/regia.css?v=1">
<%= yield :head %> <%= 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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:640px"> <div class="wrap" style="padding-top:20px;max-width:640px">
<h1>Dati di fatturazione</h1> <h1><%= t("billing.profile.heading") %></h1>
<p style="color:#aaa">Società: <strong><%= @club.name %></strong>. Inserisci i dati per lintestazione delle fatture e linvio tramite SDI o PEC.</p> <p style="color:#aaa"><%= t("billing.profile.lead", name: @club.name) %></p>
<% plan_return = params[:plan].presence_in(%w[premium_light premium_full]) %> <% plan_return = params[:plan].presence_in(%w[premium_light premium_full]) %>
<% unless @club.billing_profile_complete? %> <% unless @club.billing_profile_complete? %>
<div class="flash alert" style="margin:16px 0"> <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) %> <%= billing_profile_incomplete_message(@club) %>
<% if plan_return %> <% 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 %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -21,11 +21,11 @@
<%= hidden_field_tag :plan, plan_return if plan_return %> <%= hidden_field_tag :plan, plan_return if plan_return %>
<%= hidden_field_tag :interval, params[:interval] if params[:interval].present? %> <%= hidden_field_tag :interval, params[:interval] if params[:interval].present? %>
<%= render "shared/billing_profile_fields", record: @club %> <%= 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 %> <% end %>
</div> </div>
<p style="margin-top:20px"> <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> </p>
</div> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px"> <div class="wrap" style="padding-top:20px;max-width:560px">
<p class="results-hint" style="margin-bottom:8px"> <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> </p>
<h1>Programma partita</h1> <h1><%= t("matches.club_new.heading") %></h1>
<p style="color:#888"> <p style="color:#888">
<%= @club.name %> <%= @club.name %>
<% if @club_admin %> <% if @club_admin %>
· Puoi scegliere qualsiasi squadra della società <%= t("matches.club_new.admin_hint") %>
<% else %> <% else %>
· Solo le squadre in cui sei responsabile trasmissione <%= t("matches.club_new.staff_hint") %>
<% end %> <% end %>
</p> </p>
<div class="card" style="margin-top:16px"> <div class="card" style="margin-top:16px">
<%= form_with url: public_club_matches_path(@club), method: :post, local: true do %> <%= 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 %> <% if @schedulable_teams.size == 1 %>
<%= hidden_field_tag :team_id, @selected_team.id %> <%= hidden_field_tag :team_id, @selected_team.id %>
<p style="margin:0 0 16px;font-weight:600"><%= @selected_team.name %></p> <p style="margin:0 0 16px;font-weight:600"><%= @selected_team.name %></p>
@@ -29,7 +29,7 @@
<%= render "public/matches/schedule_fields", match: @match %> <%= 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 %> <% end %>
</div> </div>
</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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap club-dashboard" style="padding-top:20px"> <div class="wrap club-dashboard" style="padding-top:20px">
@@ -3,50 +3,46 @@
<% cred = club.youtube_credential %> <% cred = club.youtube_credential %>
<section class="card club-youtube" id="youtube"> <section class="card club-youtube" id="youtube">
<h2>YouTube Live</h2> <h2><%= t("club.youtube.heading") %></h2>
<% if params[:youtube] == "connected" %> <% 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 %> <% end %>
<% if entitlements.plan.youtube_mode == "matchlivetv_light" %> <% if entitlements.plan.youtube_mode == "matchlivetv_light" %>
<p> <p>
Con il piano <strong>Premium Light</strong> le dirette YouTube di tutte le squadre vanno sul canale ufficiale <%= raw t("club.youtube.light_body_html") %>
<strong>Match Live TV</strong>. Non serve collegare un canale della società.
</p> </p>
<% if yt&.selectable? %> <% if yt&.selectable? %>
<p class="muted">Canale pronto · seleziona «YouTube Live» nellapp mobile.</p> <p class="muted"><%= t("club.youtube.light_ready") %></p>
<% else %> <% 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 %> <% end %>
<% elsif entitlements.premium_full? %> <% elsif entitlements.premium_full? %>
<% if cred %> <% if cred %>
<p> <p>
Canale collegato per <strong>tutta la società</strong>: <%= raw t("club.youtube.full_connected_html", channel: cred.channel_title.presence || cred.channel_id || "YouTube") %>
<strong><%= cred.channel_title.presence || cred.channel_id || "YouTube" %></strong>
</p> </p>
<p class="muted">Tutte le squadre possono trasmettere su questo canale quando scelgono il canale società in app.</p> <p class="muted"><%= t("club.youtube.full_connected_hint") %></p>
<%= button_to "Scollega canale", public_club_youtube_disconnect_path(club), <%= button_to t("club.youtube.disconnect"), public_club_youtube_disconnect_path(club),
method: :delete, method: :delete,
class: "btn btn-secondary", 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 %> <% else %>
<p> <p>
Senza canale collegato, le dirette YouTube dallapp vanno sul canale ufficiale <%= raw t("club.youtube.full_not_connected_html") %>
<strong>Match Live TV</strong>.
Collega qui il canale YouTube della società (vale per tutte le squadre).
</p> </p>
<% if yt&.selectable? %> <% 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 %> <% end %>
<% if ENV["YOUTUBE_CLIENT_ID"].present? %> <% 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 %> <% else %>
<p class="muted">OAuth YouTube non ancora configurato sul server.</p> <p class="muted"><%= t("club.youtube.oauth_not_configured") %></p>
<% end %> <% end %>
<% end %> <% end %>
<% else %> <% else %>
<p class="muted">YouTube sul canale della società richiede il piano Premium Full.</p> <p class="muted"><%= t("club.youtube.requires_full") %></p>
<%= link_to "Vedi piani", public_prezzi_path, class: "btn btn-secondary" %> <%= link_to t("club.youtube.see_plans"), public_prezzi_path, class: "btn btn-secondary" %>
<% end %> <% end %>
</section> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px"> <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 %> <%= render "shared/club_subscription_status", club: @club, entitlements: @entitlements, subscription: @subscription, on_billing_page: true %>
<% if @subscription&.admin_comped? %> <% if @subscription&.admin_comped? %>
<div class="card" style="margin-top:16px;border-color:#3d3520;background:#1a1810"> <div class="card" style="margin-top:16px;border-color:#3d3520;background:#1a1810">
<p style="margin:0;color:#ddd"> <p style="margin:0;color:#ddd">
<strong>Abbonamento omaggio Match Live TV</strong> <strong><%= t("club.billing.comped.title") %></strong>
— piano <strong><%= @subscription.plan.name %></strong> <%= t("club.billing.comped.plan_prefix") %> <strong><%= @subscription.plan.name %></strong>
<% if @subscription.admin_comped_reason.present? %> <% if @subscription.admin_comped_reason.present? %>
(<%= @subscription.admin_comped_reason %>) (<%= @subscription.admin_comped_reason %>)
<% end %>. <% end %>.
Non è richiesto alcun pagamento. Per modifiche o passaggio a un piano a pagamento, contatta <%= raw t("club.billing.comped.no_payment_html", email_link: mail_to("info@matchlive.it", "info@matchlive.it")) %>
<%= mail_to "info@matchlive.it", "info@matchlive.it" %>.
</p> </p>
</div> </div>
<% else %> <% else %>
@@ -28,5 +27,5 @@
<%= render "shared/billing_documents", club: @club, payments: @payments %> <%= 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> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px"> <div class="wrap" style="padding-top:20px;max-width:560px">
<h1>Modifica società</h1> <h1><%= t("club.edit.heading") %></h1>
<div class="card"> <div class="card">
<%= form_with url: public_club_path(@club), method: :patch, multipart: true do %> <%= 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 %> <%= 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) %> <%= 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 %> <%= 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 %> <% end %>
</div> </div>
<p><%= link_to "← Società", public_club_path(@club) %></p> <p><%= link_to t("club.back_to_club"), public_club_path(@club) %></p>
</div> </div>
+20 -20
View File
@@ -1,39 +1,39 @@
<% content_for :title, "Registra società — Match Live TV" %> <% content_for :title, t("club.new.title") %>
<% content_for :meta_description, "Crea la società sportiva e la prima squadra su Match Live TV." %> <% content_for :meta_description, t("club.new.meta_description") %>
<% content_for :robots, "noindex, nofollow" %> <% content_for :robots, "noindex, nofollow" %>
<section class="auth-page"> <section class="auth-page">
<h1>La tua società</h1> <h1><%= t("club.new.heading") %></h1>
<p class="auth-lead">Registra il club: potrai aggiungere più squadre (Under 13, Under 15, Serie C…).</p> <p class="auth-lead"><%= t("club.new.lead") %></p>
<div class="card card-wide"> <div class="card card-wide">
<%= form_with url: public_clubs_path, multipart: true do %> <%= form_with url: public_clubs_path, multipart: true do %>
<h2 class="form-section-title">Società</h2> <h2 class="form-section-title"><%= t("club.new.section_club") %></h2>
<%= label_tag "club[name]", "Nome società / club" %> <%= label_tag "club[name]", t("club.new.name_label") %>
<%= text_field_tag "club[name]", params.dig(:club, :name), required: true, placeholder: "es. Crazy Volley Rozzano" %> <%= text_field_tag "club[name]", params.dig(:club, :name), required: true, placeholder: t("club.new.name_placeholder") %>
<%= label_tag "club[sport]", "Sport principale" %> <%= label_tag "club[sport]", t("club.sport_label") %>
<%= select_tag "club[sport]", sport_catalog_options(params.dig(:club, :sport) || "pallavolo") %> <%= 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") %> <%= render "shared/billing_profile_fields", record: Club.new(billing_country: "IT") %>
<h2 class="form-section-title" style="margin-top:24px">Prima squadra</h2> <h2 class="form-section-title" style="margin-top:24px"><%= t("club.new.section_first_team") %></h2>
<%= label_tag "first_team[name]", "Nome squadra" %> <%= 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: "es. Under 15" %> <%= 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([ <%= select_tag :plan, options_for_select([
["Free — 1 responsabile trasmissione per squadra, 1 live", "free"], [t("club.new.plan_free"), "free"],
["Premium Light", "premium_light"], [t("club.new.plan_light"), "premium_light"],
["Premium Full", "premium_full"] [t("club.new.plan_full"), "premium_full"]
], params[:plan] || "free"), id: "club_plan_select" %> ], params[:plan] || "free"), id: "club_plan_select" %>
<div id="club_plan_interval" style="margin-top:10px"> <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([ <%= select_tag :interval, options_for_select([
["Annuale — €40/anno (Light) o €200/anno (Full)", "yearly"], [t("club.new.interval_yearly"), "yearly"],
["Mensile — €5/mese (Light) o €20/mese (Full)", "monthly"] [t("club.new.interval_monthly"), "monthly"]
], params[:interval] || "yearly") %> ], params[:interval] || "yearly") %>
</div> </div>
<%= render "shared/stripe_secure_payment", compact: true %> <%= 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 %> <% end %>
</div> </div>
</section> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap club-dashboard" style="padding-top:20px"> <div class="wrap club-dashboard" style="padding-top:20px">
@@ -10,20 +10,20 @@
<h1><%= @club.name %></h1> <h1><%= @club.name %></h1>
<% if @entitlements %> <% if @entitlements %>
<p class="club-meta"> <p class="club-meta">
Piano: <strong><%= @entitlements.plan.name %></strong> <%= t("club.dashboard.plan_label") %> <strong><%= @entitlements.plan.name %></strong>
· Squadre: <strong><%= @teams.size %></strong> · <%= t("club.dashboard.teams_count_label") %> <strong><%= @teams.size %></strong>
</p> </p>
<% end %> <% end %>
</div> </div>
</header> </header>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin:20px 0"> <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 t("club.dashboard.edit_club"), public_edit_club_path(@club), class: "btn btn-secondary" %>
<%= link_to "Abbonamento", public_club_billing_path(@club), class: "btn btn-primary" %> <%= link_to t("club.dashboard.subscription"), 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 t("club.dashboard.new_team"), 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.live_streams"), public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
<% if @entitlements&.can_access_recordings? %> <% 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 %> <% end %>
</div> </div>
@@ -33,32 +33,32 @@
entitlements_team: @entitlements_team %> entitlements_team: @entitlements_team %>
<% if @replay_stats %> <% 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 class="card" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:16px;padding:16px;margin-bottom:24px">
<div> <div>
<div style="font-size:1.6rem;font-weight:700"><%= @replay_stats[:available_count] %></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> <div>
<div style="font-size:1.6rem;font-weight:700"><%= @replay_stats[:expiring_soon_count] %></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> <div>
<div style="font-size:1.6rem;font-weight:700"><%= number_to_human_size(@replay_stats[:total_bytes]) %></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> <div>
<div style="font-size:1.6rem;font-weight:700"><%= @replay_stats[:total_views] %></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>
</div> </div>
<% end %> <% end %>
<h2>Squadre</h2> <h2><%= t("club.dashboard.teams_heading") %></h2>
<div class="card"> <div class="card">
<% if @teams.any? %> <% if @teams.any? %>
<table class="data"> <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> <tbody>
<% @teams.each do |team| %> <% @teams.each do |team| %>
<tr> <tr>
@@ -68,28 +68,28 @@
</td> </td>
<td style="color:#888;font-size:0.88rem"> <td style="color:#888;font-size:0.88rem">
<% if team.logo_url.present? || team.logo_file.attached? || team.primary_color.present? %> <% if team.logo_url.present? || team.logo_file.attached? || team.primary_color.present? %>
Personalizzato <%= t("club.dashboard.branding_custom") %>
<% else %> <% else %>
Eredita dalla società <%= t("club.dashboard.branding_inherited") %>
<% end %> <% end %>
</td> </td>
<td style="white-space:nowrap"> <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> </td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
</table> </table>
<% else %> <% 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 %> <% end %>
</div> </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> </div>
@@ -1,21 +1,26 @@
<% content_for :title, "Invito squadra — Match Live TV" %> <% content_for :title, t("auth.invitation.meta_title") %>
<% content_for :meta_description, "Accetta l'invito per entrare nello staff della squadra su Match Live TV." %> <% content_for :meta_description, t("auth.invitation.meta_description") %>
<% content_for :robots, "noindex, nofollow" %> <% content_for :robots, "noindex, nofollow" %>
<div class="card" style="max-width:480px"> <div class="card" style="max-width:480px">
<h1>Accesso staff — <%= @invitation.team.name %></h1> <h1><%= raw t("auth.invitation.title_html", team_name: @invitation.team.name) %></h1>
<p>Sei stato aggiunto come <strong>responsabile trasmissione</strong> (<%= @invitation.email %>).</p> <p><%= raw t("auth.invitation.role_notice_html", email: @invitation.email) %></p>
<p class="muted" style="font-size:0.9rem;margin-bottom:16px"> <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> </p>
<% if logged_in? %> <% 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 %> <% else %>
<p><%= link_to "Accedi", public_login_path %> o <%= link_to "registrati", public_signup_path %> con <%= @invitation.email %>.</p> <p><%= raw t(
<%= button_to "Accetta (se già loggato)", public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %> "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 %> <% end %>
<p style="margin-top:16px;font-size:0.88rem;color:#888"> <p style="margin-top:16px;font-size:0.88rem;color:#888">
App mobile: <%= t("auth.invitation.mobile_app_label") %>
<a href="matchlivetv://join/<%= @token %>">Apri invito nellapp</a> <a href="matchlivetv://join/<%= @token %>"><%= t("auth.invitation.open_in_app") %></a>
</p> </p>
</div> </div>
@@ -5,7 +5,7 @@
badge_class = local_assigns[:badge_class] badge_class = local_assigns[:badge_class]
unless badge_label 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 end
unless badge_class 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 :title, t("live.index.meta_title") %>
<% 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 :meta_description, t("live.index.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_live_index_path) %> <% content_for :canonical_url, seo_absolute_url(public_live_index_path) %>
<% live_index_params = @club ? { club_id: @club.id } : {} %> <% live_index_params = @club ? { club_id: @club.id } : {} %>
<div class="wrap"> <div class="wrap">
<% if @club %> <% if @club %>
<p class="results-hint" style="margin-bottom:8px"> <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> </p>
<h1>Dirette e partite — <%= @club.name %></h1> <h1><%= t("live.index.club_title", name: @club.name) %></h1>
<p class="results-hint"> <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> </p>
<div class="live-actions" style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px"> <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> </div>
<% else %> <% else %>
<h1>Dirette e partite in programma</h1> <h1><%= t("live.index.default_title") %></h1>
<p class="results-hint"> <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> </p>
<div class="live-actions" style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:20px"> <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> </div>
<% end %> <% end %>
<% if @club && @can_schedule_match %> <% if @club && @can_schedule_match %>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:16px"> <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> </div>
<% end %> <% end %>
@@ -39,25 +39,25 @@
type="search" type="search"
name="q" name="q"
value="<%= @query %>" value="<%= @query %>"
placeholder="<%= @club ? "Cerca società, squadra, avversario o luogo…" : "Es. Crazy Volley, Serie D, avversario…" %>" placeholder="<%= @club ? t("live.index.search_placeholder_club") : t("live.index.search_placeholder_default") %>"
aria-label="Cerca squadra" aria-label="<%= t("live.index.search_aria_label") %>"
autocomplete="off" 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? %> <% 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 %>
<% end %> <% end %>
<% if @query.present? %> <% if @query.present? %>
<% club_suffix = @club ? t("live.index.results_hint_club_suffix", name: @club.name) : "" %>
<p class="results-hint"> <p class="results-hint">
Risultati per «<%= h @query %>»<%= @club ? " in #{@club.name}" : "" %>: <%= t("live.index.results_hint", query: @query, club_suffix: club_suffix, live_count: @sessions.size, upcoming_count: @upcoming_matches.size) %>
<%= @sessions.size %> in diretta · <%= @upcoming_matches.size %> in programma
</p> </p>
<% end %> <% end %>
<% if @sessions.any? %> <% 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"> <div class="live-grid">
<% @sessions.each do |session| %> <% @sessions.each do |session| %>
<% match = session.match %> <% match = session.match %>
@@ -72,33 +72,33 @@
<p class="card-score"> <p class="card-score">
<span class="card-sets"><%= live_score_sets_label(session.score_state) %></span> <span class="card-sets"><%= live_score_sets_label(session.score_state) %></span>
<% if live_score_partials_label(session.score_state).present? %> <% 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 %> <% end %>
<span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span> <span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span>
</p> </p>
<% end %> <% end %>
<div class="badges"> <div class="badges">
<% if on_air %> <% 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" %> <% 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" %> <% 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" %> <% elsif session.status == "reconnecting" %>
<span class="badge badge-connecting">Riconnessione</span> <span class="badge badge-connecting"><%= t("live.index.badge_reconnecting") %></span>
<% else %> <% else %>
<span class="badge badge-wait">In avvio</span> <span class="badge badge-wait"><%= t("live.index.badge_starting") %></span>
<% end %> <% end %>
</div> </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> </article>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% if @upcoming_matches.any? %> <% if @upcoming_matches.any? %>
<h2 class="section-heading<%= ' section-heading--spaced' if @sessions.any? %>">Prossime partite</h2> <h2 class="section-heading<%= ' section-heading--spaced' if @sessions.any? %>"><%= t("live.index.section_upcoming") %></h2>
<p class="results-hint">Programmate da app o sito: la diretta partirà quando lo staff avvierà la trasmissione.</p> <p class="results-hint"><%= t("live.index.upcoming_hint") %></p>
<div class="live-grid upcoming-grid"> <div class="live-grid upcoming-grid">
<% @upcoming_matches.each do |match| %> <% @upcoming_matches.each do |match| %>
<article class="live-card live-card--upcoming"> <article class="live-card live-card--upcoming">
@@ -111,7 +111,7 @@
<%= live_scheduled_relative(match.scheduled_at) %> <%= live_scheduled_relative(match.scheduled_at) %>
</p> </p>
<div class="badges"> <div class="badges">
<span class="badge badge-scheduled">In programma</span> <span class="badge badge-scheduled"><%= t("live.index.badge_scheduled") %></span>
</div> </div>
</article> </article>
<% end %> <% end %>
@@ -121,59 +121,59 @@
<% if @sessions.empty? %> <% if @sessions.empty? %>
<% if @upcoming_matches.any? %> <% if @upcoming_matches.any? %>
<div class="empty-state empty-state--soft"> <div class="empty-state empty-state--soft">
<p><strong>Nessuna diretta in questo momento.</strong></p> <p><strong><%= t("live.index.empty_soft_title") %></strong></p>
<p>Torna allorario indicato: quando la squadra avvierà lo streaming, la partita comparirà in «In diretta adesso».</p> <p><%= t("live.index.empty_soft_body") %></p>
</div> </div>
<% elsif @club %> <% elsif @club %>
<div class="empty-state"> <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> <p>
<% if @can_schedule_match %> <% if @can_schedule_match %>
<%= link_to "Programma la prima partita", public_new_club_match_path(@club), class: "btn btn-primary" %> <%= link_to t("live.index.empty_club_schedule_link"), public_new_club_match_path(@club), class: "btn btn-primary" %>
oppure dallapp Match Live TV. <%= t("live.index.empty_club_schedule_suffix") %>
<% elsif @club.teams.any? %> <% 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 %> <% else %>
Aggiungi una squadra dalla <%= raw t(
<%= link_to "pagina società", public_club_path(@club) %> "live.index.empty_club_add_team_html",
e programma la prima partita. club_link: link_to(t("live.index.empty_club_page_link"), public_club_path(@club))
) %>
<% end %> <% end %>
</p> </p>
<p style="margin-top:16px"> <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> </p>
</div> </div>
<% else %> <% else %>
<div class="empty-hero"> <div class="empty-hero">
<div class="empty-hero-copy"> <div class="empty-hero-copy">
<h2>Nessuna diretta adesso — ma puoi già organizzarti</h2> <h2><%= t("live.index.empty_hero_title") %></h2>
<p> <p>
Se la tua squadra usa Match Live TV, dal telefono si programma la partita con data e ora. <%= t("live.index.empty_hero_body") %>
Genitori e parenti trovano qui la diretta quando il fischio dinizio è vicino.
</p> </p>
<ul class="empty-hero-list"> <ul class="empty-hero-list">
<li>Programma la gara dallapp (senza avviare subito la diretta)</li> <li><%= t("live.index.empty_hero_list_item1") %></li>
<li>La partita compare in «Prossime partite» con orario e campo</li> <li><%= t("live.index.empty_hero_list_item2") %></li>
<li>Lo streaming parte solo quando premi avvio in palestra</li> <li><%= t("live.index.empty_hero_list_item3") %></li>
</ul> </ul>
<p class="empty-hero-cta"> <p class="empty-hero-cta">
<%= link_to "Registra la tua squadra", public_signup_path, class: "btn btn-primary" %> <%= link_to t("live.index.empty_hero_cta_signup"), 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_features"), public_features_path, class: "btn btn-secondary" %>
</p> </p>
</div> </div>
<aside class="demo-live-card" aria-label="Esempio di diretta attiva"> <aside class="demo-live-card" aria-label="<%= t("live.index.demo_aria_label") %>">
<p class="demo-label">Esempio — così appare una diretta attiva</p> <p class="demo-label"><%= t("live.index.demo_label") %></p>
<article class="live-card live-card--demo"> <article class="live-card live-card--demo">
<h3>Tigers Volley vs ASD Eagles</h3> <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"> <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> <span class="card-points">18 - 16</span>
</p> </p>
<div class="badges"> <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> </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> </article>
</aside> </aside>
</div> </div>
+32 -26
View File
@@ -1,6 +1,12 @@
<% club_name = @match.team.club&.name %> <% club_name = @match.team.club&.name %>
<% content_for :title do %><%= club_name %> · <%= @match.team.name %> vs <%= @match.opponent_name %> — Diretta live<% 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 %>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 :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 :robots, @session.publicly_listed? ? "index, follow" : "noindex, nofollow" %>
<% content_for :head do %> <% content_for :head do %>
@@ -10,45 +16,45 @@
<% end %> <% end %>
<div class="wrap"> <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) %> <%= live_match_page_heading(@match) %>
<% if @stream_closed %> <% if @stream_closed %>
<div id="stream-ended" class="stream-ended" role="status" aria-live="polite"> <div id="stream-ended" class="stream-ended" role="status" aria-live="polite">
<div class="stream-ended-icon" aria-hidden="true">📡</div> <div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2>Diretta terminata</h2> <h2><%= t("live.show.stream_ended_title") %></h2>
<p>Lo streaming di questa partita è stato chiuso.</p> <p><%= t("live.show.stream_ended_body") %></p>
<% if @session.ended_at %> <% 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 %> <% end %>
<p class="stream-ended-hint">Il punteggio è visibile nel video se la diretta era ancora in corso.</p> <p class="stream-ended-hint"><%= t("live.show.stream_ended_hint") %></p>
<%= link_to "Tutte le dirette", public_live_index_path, class: "btn btn-secondary stream-ended-btn" %> <%= link_to t("live.show.all_live_link"), public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
</div> </div>
<% elsif @session.platform == "youtube" %> <% elsif @session.platform == "youtube" %>
<div class="stream-ended youtube-live-cta" role="status" aria-live="polite"> <div class="stream-ended youtube-live-cta" role="status" aria-live="polite">
<div class="stream-ended-icon" aria-hidden="true"></div> <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? %> <% if @session.youtube_watch_url.present? %>
<p>Questa partita è trasmessa sul canale YouTube Match Live TV, non sul player del sito.</p> <p><%= t("live.show.youtube_available_body") %></p>
<%= link_to "Guarda su YouTube", @session.youtube_watch_url, class: "btn stream-ended-btn", target: "_blank", rel: "noopener" %> <%= link_to t("live.show.youtube_watch_link"), @session.youtube_watch_url, class: "btn stream-ended-btn", target: "_blank", rel: "noopener" %>
<% else %> <% 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"> <meta http-equiv="refresh" content="8">
<% end %> <% 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> </div>
<% else %> <% else %>
<div class="live-player-wrap"> <div class="live-player-wrap">
<video id="player" controls playsinline muted autoplay poster="/images/copertina-canale.png"></video> <video id="player" controls playsinline muted autoplay poster="/images/copertina-canale.png"></video>
<%= render "public/live/player_overlays", match: @match, session: @session %> <%= render "public/live/player_overlays", match: @match, session: @session %>
<button type="button" id="play-hint" class="live-play-hint" hidden> <button type="button" id="play-hint" class="live-play-hint" hidden>
▶ Avvia la diretta <%= t("live.show.play_hint") %>
</button> </button>
</div> </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="offline-msg" class="live-status-msg" hidden><%= t("live.show.offline_msg") %></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="awaiting-msg" class="live-status-msg" hidden><%= t("live.show.awaiting_msg") %></p>
<p id="paused-msg" class="live-status-msg" hidden>Trasmissione in pausa — il flusso continua (copertina server).</p> <p id="paused-msg" class="live-status-msg" hidden><%= t("live.show.paused_msg") %></p>
<script> <script>
const sessionId = "<%= @session.id %>"; const sessionId = "<%= @session.id %>";
@@ -63,19 +69,19 @@
function syncStreamBadge(data) { function syncStreamBadge(data) {
if (!streamBadge) return; if (!streamBadge) return;
if (data.stream_closed) { 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"; streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-ended";
return; return;
} }
const paused = !!(data.paused || data.status === "paused"); const paused = !!(data.paused || data.status === "paused");
if (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"; streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
} else if (data.on_air) { } 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"; streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-live";
} else { } else {
streamBadge.textContent = "In attesa"; streamBadge.textContent = "<%= j t("live.overlays.badge_waiting") %>";
streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait"; streamBadge.className = "live-ovl-badge live-ovl-badge--right badge-wait";
} }
} }
@@ -158,10 +164,10 @@
panel.setAttribute("role", "status"); panel.setAttribute("role", "status");
panel.innerHTML = ` panel.innerHTML = `
<div class="stream-ended-icon" aria-hidden="true">📡</div> <div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2>Diretta terminata</h2> <h2><%= j t("live.show.stream_ended_title") %></h2>
<p>Lo streaming di questa partita è stato chiuso.</p> <p><%= j t("live.show.stream_ended_body") %></p>
<p class="stream-ended-hint">Ricarica la pagina per lultimo stato registrato.</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">Tutte le dirette</a> <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); video.parentNode.insertBefore(panel, video.nextSibling);
} }
@@ -277,7 +283,7 @@
offlineMsg.hidden = true; offlineMsg.hidden = true;
if (!publisherOnline && showingCover && !paused) { if (!publisherOnline && showingCover && !paused) {
awaitingMsg.hidden = false; awaitingMsg.hidden = false;
awaitingMsg.textContent = "In riconnessione - in attesa del video dall'evento"; awaitingMsg.textContent = "<%= j t("live.show.js_reconnecting_awaiting") %>";
} else { } else {
awaitingMsg.hidden = !awaitingSignal; awaitingMsg.hidden = !awaitingSignal;
} }
@@ -1,13 +1,13 @@
<%= label_tag "match[opponent_name]", "Avversario" %> <%= label_tag "match[opponent_name]", t("matches.opponent_label") %>
<%= text_field_tag "match[opponent_name]", match.opponent_name, required: true, placeholder: "es. Volley Milano" %> <%= text_field_tag "match[opponent_name]", match.opponent_name, required: true, placeholder: t("matches.opponent_placeholder") %>
<%= label_tag "match[location]", "Luogo (opzionale)" %> <%= label_tag "match[location]", t("matches.location_label") %>
<%= text_field_tag "match[location]", match.location, placeholder: "Palestra, città" %> <%= 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") %> <% 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 %> <%= datetime_local_field_tag "match[scheduled_at]", scheduled_value, required: true %>
<p style="color:#888;font-size:0.88rem;margin:12px 0 0"> <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> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px"> <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> <p style="color:#888"><%= @team.name %> vs <strong><%= @match.opponent_name %></strong></p>
<div class="card" style="margin-top:16px"> <div class="card" style="margin-top:16px">
<%= render "form", <%= render "form",
form_url: public_team_match_path(@team, @match), form_url: public_team_match_path(@team, @match),
form_method: :patch, form_method: :patch,
submit_label: "Salva modifiche" %> submit_label: t("matches.edit.submit") %>
</div> </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> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:720px"> <div class="wrap" style="padding-top:20px;max-width:720px">
@@ -7,17 +7,16 @@
</nav> </nav>
<header style="margin:16px 0 20px"> <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"> <p style="color:#888;margin-top:8px">
Programma qui da PC data, avversario e luogo. Quando sei in palestra apri lapp Match Live TV, <%= t("matches.index.lead") %>
seleziona questa squadra e avvia la diretta sulla partita.
</p> </p>
</header> </header>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:20px"> <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 t("matches.index.schedule_match"), 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 t("matches.index.team_details"), 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.live_and_schedule"), public_live_index_path(club_id: @club.id), class: "btn btn-secondary" %>
</div> </div>
<div class="card"> <div class="card">
@@ -25,10 +24,10 @@
<table class="data"> <table class="data">
<thead> <thead>
<tr> <tr>
<th>Avversario</th> <th><%= t("matches.index.col_opponent") %></th>
<th>Data e ora</th> <th><%= t("matches.index.col_datetime") %></th>
<th>Luogo</th> <th><%= t("matches.index.col_location") %></th>
<th>Stato</th> <th><%= t("matches.index.col_status") %></th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -40,25 +39,25 @@
<% if match.scheduled_at %> <% if match.scheduled_at %>
<%= l_local(match.scheduled_at) %> <%= l_local(match.scheduled_at) %>
<% else %> <% else %>
<span style="color:#888">Senza orario</span> <span style="color:#888"><%= t("matches.index.no_time") %></span>
<% end %> <% end %>
</td> </td>
<td><%= match.location.presence || "—" %></td> <td><%= match.location.presence || "—" %></td>
<td> <td>
<% label = match.public_status_label %> <% 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> <span style="color:#e53935;font-weight:600"><%= label %></span>
<% else %> <% else %>
<%= label %> <%= label %>
<% end %> <% end %>
</td> </td>
<td style="white-space:nowrap"> <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? %> <% 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, 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", class: "btn btn-secondary",
style: "padding:4px 10px;font-size:0.8rem;display:inline" %> style: "padding:4px 10px;font-size:0.8rem;display:inline" %>
<% end %> <% end %>
@@ -69,8 +68,7 @@
</table> </table>
<% else %> <% else %>
<p style="color:#888;margin:0"> <p style="color:#888;margin:0">
Nessuna partita in calendario. <%= raw t("matches.index.empty_html", link: link_to(t("matches.index.schedule_first"), new_public_team_match_path(@team))) %>
<%= link_to "Programma la prima", new_public_team_match_path(@team) %>.
</p> </p>
<% end %> <% end %>
</div> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:560px"> <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> <p style="color:#888"><%= @club.name %> · <strong><%= @team.name %></strong></p>
<div class="card" style="margin-top:16px"> <div class="card" style="margin-top:16px">
<%= render "form", <%= render "form",
form_url: public_team_matches_path(@team), form_url: public_team_matches_path(@team),
form_method: :post, form_method: :post,
submit_label: "Salva in programma" %> submit_label: t("matches.new.submit") %>
</div> </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> </div>
+65 -69
View File
@@ -1,150 +1,146 @@
<% content_for :title, "Cookie policy — Match Live TV" %> <% content_for :title, t("legal.cookies.title") %>
<% content_for :meta_description, "Cookie policy di Match Live TV: cookie tecnici, sessione, Google Analytics e gestione del consenso." %> <% content_for :meta_description, t("legal.cookies.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_cookies_path) %> <% content_for :canonical_url, seo_absolute_url(public_cookies_path) %>
<div class="wrap legal-doc"> <div class="wrap legal-doc">
<h1>Cookie policy</h1> <h1><%= t("legal.cookies.h1") %></h1>
<p class="legal-meta"> <p class="legal-meta">
Ultimo aggiornamento: <%= cookie_policy_last_updated %> <%= raw t(
· Vedi anche <%= link_to "informativa privacy", public_privacy_path %> "legal.cookies.meta_html",
date: cookie_policy_last_updated,
privacy_link: link_to(t("legal.cookies.privacy_link_text"), public_privacy_path)
) %>
</p> </p>
<section> <section>
<h2>1. Cosa sono i cookie</h2> <h2><%= t("legal.cookies.s1_title") %></h2>
<p> <p>
I cookie sono piccoli file di testo che i siti salvano sul dispositivo del visitatore. <%= t("legal.cookies.s1_body") %>
Servono a far funzionare il sito, ricordare preferenze o, previo consenso, analizzare luso del servizio.
</p> </p>
</section> </section>
<section> <section>
<h2>2. Titolare</h2> <h2><%= t("legal.cookies.s2_title") %></h2>
<p> <p>
<strong><%= MatchLiveTv.privacy_controller_name %></strong> <strong><%= MatchLiveTv.privacy_controller_name %></strong>
<%= MatchLiveTv.privacy_controller_address %> <%= MatchLiveTv.privacy_controller_address %>
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a> <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>
<% if MatchLiveTv.privacy_controller_vat.present? %> <% if MatchLiveTv.privacy_controller_vat.present? %>
· P. IVA <%= MatchLiveTv.privacy_controller_vat %> · <%= t("legal.cookies.s2_vat_label") %> <%= MatchLiveTv.privacy_controller_vat %>
<% end %> <% end %>
</p> </p>
</section> </section>
<section> <section>
<h2>3. Come gestiamo il consenso</h2> <h2><%= t("legal.cookies.s3_title") %></h2>
<p> <p>
Al primo accesso mostriamo un banner che ti permette di accettare tutti i cookie, <%= raw t("legal.cookies.s3_p1_html") %>
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.
</p> </p>
<p> <p>
La preferenza viene memorizzata in un cookie tecnico di prima parte <%= raw t("legal.cookies.s3_p2_html") %>
(<code>mltv_cookie_consent</code>) e in <code>localStorage</code> del browser.
</p> </p>
</section> </section>
<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> <h3><%= t("legal.cookies.s4_1_title") %></h3>
<p>Non richiedono consenso perché indispensabili al funzionamento del sito e dellarea riservata.</p> <p><%= t("legal.cookies.s4_1_intro") %></p>
<table class="legal-table"> <table class="legal-table">
<thead> <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> </thead>
<tbody> <tbody>
<tr> <tr>
<td><code>_app_session</code> (cookie di sessione)</td> <td><%= raw t("legal.cookies.s4_1_row1_name_html") %></td>
<td>Mantiene laccesso dellutente registrato, protezione CSRF, preferenze di navigazione sicura</td> <td><%= t("legal.cookies.s4_1_row1_purpose") %></td>
<td>Sessione / fino a chiusura browser</td> <td><%= t("legal.cookies.s4_1_row1_duration") %></td>
<td>Match Live TV (prima parte)</td> <td><%= t("legal.cookies.s4_1_row1_provider") %></td>
</tr> </tr>
<tr> <tr>
<td><code>mltv_cookie_consent</code></td> <td><%= raw t("legal.cookies.s4_1_row2_name_html") %></td>
<td>Memorizza le scelte sul banner cookie (necessari / analytics)</td> <td><%= t("legal.cookies.s4_1_row2_purpose") %></td>
<td>12 mesi</td> <td><%= t("legal.cookies.s4_1_row2_duration") %></td>
<td>Match Live TV (prima parte)</td> <td><%= t("legal.cookies.s4_1_row2_provider") %></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<h3>4.2 Analytics (solo con il tuo consenso)</h3> <h3><%= t("legal.cookies.s4_2_title") %></h3>
<p> <p>
Se accetti «Tutti i cookie» o abiliti le statistiche nel banner, carichiamo <%= raw t("legal.cookies.s4_2_p1_html") %>
<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.
</p> </p>
<% if MatchLiveTv.google_analytics_configured? %> <% 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 %> <% 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 %> <% end %>
<table class="legal-table"> <table class="legal-table">
<thead> <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> </thead>
<tbody> <tbody>
<tr> <tr>
<td><code>_ga</code></td> <td><%= raw t("legal.cookies.s4_2_row1_name_html") %></td>
<td>Distingue gli utenti (statistiche)</td> <td><%= t("legal.cookies.s4_2_row1_purpose") %></td>
<td>2 anni</td> <td><%= t("legal.cookies.s4_2_row1_duration") %></td>
<td>Google</td> <td><%= t("legal.cookies.s4_2_row1_provider") %></td>
</tr> </tr>
<tr> <tr>
<td><code>_ga_*</code></td> <td><%= raw t("legal.cookies.s4_2_row2_name_html") %></td>
<td>Mantiene lo stato della sessione Analytics</td> <td><%= t("legal.cookies.s4_2_row2_purpose") %></td>
<td>2 anni</td> <td><%= t("legal.cookies.s4_2_row2_duration") %></td>
<td>Google</td> <td><%= t("legal.cookies.s4_2_row2_provider") %></td>
</tr> </tr>
<tr> <tr>
<td><code>_gid</code></td> <td><%= raw t("legal.cookies.s4_2_row3_name_html") %></td>
<td>Distingue gli utenti (statistiche)</td> <td><%= t("legal.cookies.s4_2_row3_purpose") %></td>
<td>24 ore</td> <td><%= t("legal.cookies.s4_2_row3_duration") %></td>
<td>Google</td> <td><%= t("legal.cookies.s4_2_row3_provider") %></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
<p> <p>
Puoi revocare il consenso dal banner o dalle impostazioni del browser. <%= raw t(
Informazioni Google: "legal.cookies.s4_2_p2_html",
<a href="https://policies.google.com/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy Google</a>, google_privacy_link: link_to(t("legal.cookies.s4_2_google_privacy_link_text"), "https://policies.google.com/privacy", target: "_blank", rel: "noopener noreferrer"),
<a href="https://tools.google.com/dlpage/gaoptout" target="_blank" rel="noopener noreferrer">componente opt-out Analytics</a>. 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> </p>
<h3>4.3 Cookie di terze parti durante il pagamento</h3> <h3><%= t("legal.cookies.s4_3_title") %></h3>
<p> <p>
Se acquisti un piano Premium, vieni reindirizzato a <strong>Stripe</strong> (checkout sicuro). <%= raw t(
Stripe può impostare cookie propri sul dominio <code>stripe.com</code> per prevenire frodi e completare il pagamento. "legal.cookies.s4_3_body_html",
Non controlliamo direttamente tali cookie: consulta la stripe_link: link_to(t("legal.cookies.s4_3_stripe_link_text"), "https://stripe.com/privacy", target: "_blank", rel: "noopener noreferrer")
<a href="https://stripe.com/privacy" target="_blank" rel="noopener noreferrer">privacy policy di Stripe</a>. ) %>
</p> </p>
<h3>4.4 YouTube e servizi collegati</h3> <h3><%= t("legal.cookies.s4_4_title") %></h3>
<p> <p>
Lintegrazione YouTube per le dirette avviene tramite API lato server; il sito pubblico non incorpora <%= t("legal.cookies.s4_4_body") %>
player YouTube con cookie di profilazione. I visitatori che aprono un link YouTube esterno
sono soggetti alle policy di Google/YouTube su quel dominio.
</p> </p>
</section> </section>
<section> <section>
<h2>5. Come disabilitare i cookie dal browser</h2> <h2><%= t("legal.cookies.s5_title") %></h2>
<p> <p>
Puoi bloccare o cancellare i cookie dalle impostazioni del browser (Chrome, Firefox, Safari, Edge). <%= t("legal.cookies.s5_body") %>
Disabilitando i cookie necessari alcune funzioni (es. login) potrebbero non funzionare.
</p> </p>
</section> </section>
<section> <section>
<h2>6. Diritti e contatti</h2> <h2><%= t("legal.cookies.s6_title") %></h2>
<p> <p>
Per esercitare i diritti previsti dal GDPR (accesso, cancellazione, opposizione, revoca consenso) <%= raw t(
scrivi a <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>. "legal.cookies.s6_body_html",
Dettagli nel <%= link_to "documento privacy", public_privacy_path %>. 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> </p>
</section> </section>
<p style="margin-top:24px"> <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> </p>
</div> </div>
+28 -32
View File
@@ -1,83 +1,79 @@
<% content_for :title, "FAQ — Diretta live partite giovanili | Match Live TV" %> <% content_for :title, t("pages.faq.meta_title") %>
<% 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 :meta_description, t("pages.faq.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_faq_path) %> <% content_for :canonical_url, seo_absolute_url(public_faq_path) %>
<div class="wrap seo-page"> <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"> <p class="seo-lead">
Risposte per genitori, allenatori e dirigenti che vogliono mandare in diretta live le partite <%= t("pages.faq.lead") %>
di pallavolo, calcio e altri sport dilettantistici.
</p> </p>
<div class="faq-list"> <div class="faq-list">
<details class="faq-item" open> <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> <p>
La società condivide un link (via WhatsApp, email o gruppo squadra). Apri il link dal telefono o dal computer: <%= raw t("pages.faq.q1_answer_html") %>
vedi la <strong>diretta live</strong> nel browser, senza registrarti. Funziona per nonni, parenti e amici lontani.
</p> </p>
</details> </details>
<details class="faq-item"> <details class="faq-item">
<summary>Chi può avviare la diretta dalla palestra?</summary> <summary><%= t("pages.faq.q2_question") %></summary>
<p> <p>
Coach, dirigenti o volontari invitati dalla società. Usano lapp Match Live TV sul telefono per filmare e, <%= t("pages.faq.q2_answer") %>
se serve, un secondo telefono per aggiornare il punteggio. La diretta parte solo quando premo avvio in palestra.
</p> </p>
</details> </details>
<details class="faq-item"> <details class="faq-item">
<summary>È adatto alla pallavolo giovanile (Under 14, Under 16)?</summary> <summary><%= t("pages.faq.q3_question") %></summary>
<p> <p>
Sì. Il punteggio a set è pensato per la pallavolo; puoi personalizzare regole per tornei particolari. <%= raw t(
Scopri la pagina dedicata: <%= link_to "Match Live TV per pallavolo giovanile", public_pallavolo_path %>. "pages.faq.q3_answer_html",
volleyball_link: link_to(t("pages.faq.q3_volleyball_link"), public_pallavolo_path)
) %>
</p> </p>
</details> </details>
<details class="faq-item"> <details class="faq-item">
<summary>Cosa succede se perdo la diretta?</summary> <summary><%= t("pages.faq.q4_question") %></summary>
<p> <p>
Con i piani Premium Light o Full la partita viene salvata in archivio (30 o 90 giorni) e puoi rivederla dal sito. <%= t("pages.faq.q4_answer") %>
Il piano Free non include replay su server, ma la diretta resta gratuita per chi trasmette e per chi guarda.
</p> </p>
</details> </details>
<details class="faq-item"> <details class="faq-item">
<summary>Quanto costa per la società?</summary> <summary><%= t("pages.faq.q5_question") %></summary>
<p> <p>
Puoi iniziare con il piano <strong>Free</strong> (limiti su staff e una diretta alla volta). <%= raw t(
Premium Light e Full aggiungono più partite in parallelo, archivio e YouTube. "pages.faq.q5_answer_html",
<%= link_to "Confronta i prezzi", public_prezzi_path %>. pricing_link: link_to(t("pages.faq.q5_pricing_link"), public_prezzi_path)
) %>
</p> </p>
</details> </details>
<details class="faq-item"> <details class="faq-item">
<summary>Posso programmare una partita prima del giorno gara?</summary> <summary><%= t("pages.faq.q6_question") %></summary>
<p> <p>
Sì, dallapp si imposta data e ora: la partita compare in «Prossime partite» sul sito. <%= t("pages.faq.q6_answer") %>
Lo streaming parte solo quando lo staff avvia la trasmissione in palestra.
</p> </p>
</details> </details>
<details class="faq-item"> <details class="faq-item">
<summary>La diretta va anche su YouTube?</summary> <summary><%= t("pages.faq.q7_question") %></summary>
<p> <p>
Con Premium Light puoi mandarla sul canale Match Live TV; con Premium Full anche sul canale YouTube della società. <%= t("pages.faq.q7_answer") %>
Chi preferisce resta sul link Match Live TV, comodo per le famiglie.
</p> </p>
</details> </details>
<details class="faq-item"> <details class="faq-item">
<summary>Devo aprire porte sul router o avere attrezzatura da TV?</summary> <summary><%= t("pages.faq.q8_question") %></summary>
<p> <p>
No per chi guarda. Per la società che trasmette basta lo smartphone e una buona connessione in palestra; <%= t("pages.faq.q8_answer") %>
lo staff tecnico della piattaforma gestisce linfrastruttura. Niente camere broadcast o mixer.
</p> </p>
</details> </details>
</div> </div>
<p style="text-align:center;margin:40px 0"> <p style="text-align:center;margin:40px 0">
<%= link_to "Registra la squadra", public_signup_path, class: "btn btn-primary" %> <%= link_to t("pages.faq.cta_signup"), 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_live"), public_live_index_path, class: "btn btn-secondary" %>
</p> </p>
</div> </div>
@@ -1,64 +1,64 @@
<% content_for :title, "Funzionalità streaming partite giovanili — Match Live TV" %> <% content_for :title, t("pages.features.meta_title") %>
<% 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 :meta_description, t("pages.features.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_features_path) %> <% content_for :canonical_url, seo_absolute_url(public_features_path) %>
<div class="wrap"> <div class="wrap">
<h1 style="margin-top:24px">Funzionalità</h1> <h1 style="margin-top:24px"><%= t("pages.features.title") %></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> <p style="color:#aaa;max-width:640px"><%= t("pages.features.lead") %></p>
<section class="section"> <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="features-grid">
<div class="feature-card"> <div class="feature-card">
<h3>Inviti con la propria email</h3> <h3><%= t("pages.features.staff_invite_title") %></h3>
<p>Il presidente o lowner invita chi filma e chi aggiorna il punteggio: ognuno accede con la sua email, senza condividere password.</p> <p><%= t("pages.features.staff_invite_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Gestione accessi</h3> <h3><%= t("pages.features.staff_access_title") %></h3>
<p>Puoi togliere laccesso a un volontario o annullare un invito ancora in sospeso dalla dashboard della squadra.</p> <p><%= t("pages.features.staff_access_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Piani su misura</h3> <h3><%= t("pages.features.staff_plans_title") %></h3>
<p>Con Free, Premium Light o Premium Full aumenti quante persone possono aiutare in diretta e quante partite possono andare in parallelo.</p> <p><%= t("pages.features.staff_plans_body") %></p>
</div> </div>
</div> </div>
</section> </section>
<section class="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="features-grid">
<div class="feature-card"> <div class="feature-card">
<h3>Filma dal telefono</h3> <h3><%= t("pages.features.live_phone_title") %></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> <p><%= t("pages.features.live_phone_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Punteggio da un secondo telefono</h3> <h3><%= t("pages.features.live_score_title") %></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> <p><%= t("pages.features.live_score_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Link per famiglie e tifosi</h3> <h3><%= t("pages.features.live_link_title") %></h3>
<p>Condividi un link: genitori, nonni e amici guardano dal browser del telefono o del computer, senza installare app.</p> <p><%= t("pages.features.live_link_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Archivio partite</h3> <h3><%= t("pages.features.live_archive_title") %></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> <p><%= t("pages.features.live_archive_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Anche su YouTube (Match Live TV)</h3> <h3><%= t("pages.features.live_youtube_mltv_title") %></h3>
<p>Con Premium Light la diretta è visibile anche sul canale YouTube Match Live TV, oltre al link sul sito.</p> <p><%= t("pages.features.live_youtube_mltv_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Canale YouTube della società</h3> <h3><%= t("pages.features.live_youtube_club_title") %></h3>
<p>Con Premium Full puoi mandare la stessa diretta anche sul canale YouTube ufficiale del club.</p> <p><%= t("pages.features.live_youtube_club_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Sul sito della società</h3> <h3><%= t("pages.features.live_website_title") %></h3>
<p>Integrazione per incorporare il player sul sito del club — in arrivo con Premium Full.</p> <p><%= t("pages.features.live_website_body") %></p>
</div> </div>
</div> </div>
</section> </section>
<p style="text-align:center;margin:40px 0"> <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> </p>
</div> </div>
@@ -1,67 +1,60 @@
<% content_for :title, "Diretta live pallavolo giovanile — Under 14, Under 16 | Match Live TV" %> <% content_for :title, t("pages.volleyball.meta_title") %>
<% 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 :meta_description, t("pages.volleyball.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_pallavolo_path) %> <% content_for :canonical_url, seo_absolute_url(public_pallavolo_path) %>
<div class="wrap seo-page"> <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"> <p class="seo-lead">
La soluzione per società, coach e genitori che vogliono una <strong>diretta live</strong> affidabile <%= raw t("pages.volleyball.lead_html") %>
delle partite <strong>Under 14, Under 16, Under 18</strong> e settori giovanili — senza diventare telecronisti.
</p> </p>
<section class="seo-prose"> <section class="seo-prose">
<h2>Perché la pallavolo giovanile ha bisogno di uno streaming semplice</h2> <h2><%= t("pages.volleyball.section1_title") %></h2>
<p> <p>
In palestra i genitori non sono sempre tutti in tribuna: turni di lavoro, fratelli piccoli, trasferte. <%= raw t("pages.volleyball.section1_body_html") %>
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.
</p> </p>
<h2>Cosa fa il coach in due minuti</h2> <h2><%= t("pages.volleyball.section2_title") %></h2>
<ul> <ul>
<li>Programma la partita con data, avversario e campo dallapp</li> <li><%= t("pages.volleyball.section2_item1") %></li>
<li>Il giorno gara appoggia il telefono e avvia la diretta</li> <li><%= t("pages.volleyball.section2_item2") %></li>
<li>Un volontario può aggiornare set e punti da un secondo cellulare</li> <li><%= t("pages.volleyball.section2_item3") %></li>
<li>Condivide il link con il gruppo genitori: tutti guardano dal browser</li> <li><%= t("pages.volleyball.section2_item4") %></li>
</ul> </ul>
<h2>Cosa vede la famiglia a casa</h2> <h2><%= t("pages.volleyball.section3_title") %></h2>
<p> <p>
Nessuna app da installare. Apri il link e segui la partita in diretta live: nome squadre, punteggio, <%= t("pages.volleyball.section3_body") %>
stato del set. Se hai perso linizio, con Premium la gara resta in archivio per rivederla quando vuoi.
</p> </p>
<h2>Pallavolo: punteggio e regole</h2> <h2><%= t("pages.volleyball.section4_title") %></h2>
<p> <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): <%= t("pages.volleyball.section4_body") %>
in campo resta concentrato sul gioco, non sul telefono.
</p> </p>
<h2>Altri sport giovanili</h2> <h2><%= t("pages.volleyball.section5_title") %></h2>
<p> <p>
La stessa piattaforma funziona anche per calcio, basket e sport di squadra dilettantistici. <%= t("pages.volleyball.section5_body") %>
La pallavolo resta lo sport con cui molte società iniziano perché il punteggio a set è già integrato.
</p> </p>
</section> </section>
<div class="features-grid" style="margin-top:32px"> <div class="features-grid" style="margin-top:32px">
<div class="feature-card"> <div class="feature-card">
<h3>Genitori e nonni</h3> <h3><%= t("pages.volleyball.card1_title") %></h3>
<p>Link su WhatsApp, visione da telefono o TV con browser. Zero registrazione per chi guarda.</p> <p><%= t("pages.volleyball.card1_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Società e staff</h3> <h3><%= t("pages.volleyball.card2_title") %></h3>
<p>Inviti con email, più operatori con Premium, revoca accessi dalla dashboard web.</p> <p><%= t("pages.volleyball.card2_body") %></p>
</div> </div>
<div class="feature-card"> <div class="feature-card">
<h3>Archivio gare</h3> <h3><%= t("pages.volleyball.card3_title") %></h3>
<p>Premium Light/Full: replay per chi ha perso la diretta o vuole rivedere un punto chiave.</p> <p><%= t("pages.volleyball.card3_body") %></p>
</div> </div>
</div> </div>
<p style="text-align:center;margin:40px 0"> <p style="text-align:center;margin:40px 0">
<%= link_to "Inizia gratis", public_signup_path, class: "btn btn-primary" %> <%= link_to t("pages.volleyball.cta_start"), public_signup_path, class: "btn btn-primary" %>
<%= link_to "Domande frequenti", public_faq_path, class: "btn btn-secondary" %> <%= link_to t("pages.volleyball.cta_faq"), public_faq_path, class: "btn btn-secondary" %>
</p> </p>
</div> </div>
+20 -16
View File
@@ -1,18 +1,22 @@
<% content_for :title, "Prezzi streaming partite giovanili — Match Live TV" %> <% content_for :title, t("pages.pricing.meta_title") %>
<% 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 :meta_description, t("pages.pricing.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_prezzi_path) %> <% content_for :canonical_url, seo_absolute_url(public_prezzi_path) %>
<div class="wrap" style="padding-top:24px"> <div class="wrap" style="padding-top:24px">
<% if @club %> <% if @club %>
<h1>Piani per la tua società</h1> <h1><%= t("pages.pricing.club_title") %></h1>
<% if @entitlements %> <% if @entitlements %>
<%= render "shared/club_subscription_status", club: @club, entitlements: @entitlements, subscription: @subscription %> <%= render "shared/club_subscription_status", club: @club, entitlements: @entitlements, subscription: @subscription %>
<% else %> <% 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 %> <% end %>
<% else %> <% else %>
<h1>Piani per la tua squadra</h1> <h1><%= t("pages.pricing.team_title") %></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> <p style="color:#aaa"><%= t("pages.pricing.team_lead") %></p>
<% end %> <% end %>
<%= render "shared/stripe_secure_payment" %> <%= render "shared/stripe_secure_payment" %>
@@ -23,19 +27,19 @@
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr> <tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr>
</thead> </thead>
<tbody> <tbody>
<tr><td>Responsabili trasmissione / anno</td><td>1</td><td>5</td><td>Illimitato</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>Partite in contemporanea</td><td>1</td><td>3</td><td>10</td></tr> <tr><td><%= t("pages.pricing.table_matches") %></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><%= 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>YouTube</td><td>No</td><td>Match Live TV</td><td>Canale società</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>Replay server</td><td>No</td><td>30 gg</td><td>90 gg</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>Download telefono</td><td>No</td><td></td><td></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>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_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> </tbody>
</table> </table>
<div class="card" style="margin-top:32px;text-align:center"> <div class="card" style="margin-top:32px;text-align:center">
<h3 style="margin-top:0">Esigenze diverse?</h3> <h3 style="margin-top:0"><%= t("pages.pricing.different_title") %></h3>
<p style="color:#aaa;margin-bottom:16px">Per società con più squadre, tornei o integrazioni custom, contattaci: troviamo insieme l'offerta migliore.</p> <p style="color:#aaa;margin-bottom:16px"><%= t("pages.pricing.different_body") %></p>
<%= mail_to "info@matchlive.it", "Contattaci", class: "btn btn-primary" %> <%= mail_to "info@matchlive.it", t("pages.pricing.contact_button"), class: "btn btn-primary" %>
</div> </div>
</div> </div>
+109 -101
View File
@@ -1,192 +1,200 @@
<% content_for :title, "Informativa privacy — Match Live TV" %> <% content_for :title, t("legal.privacy.title") %>
<% 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 :meta_description, t("legal.privacy.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_privacy_path) %> <% content_for :canonical_url, seo_absolute_url(public_privacy_path) %>
<div class="wrap legal-doc"> <div class="wrap legal-doc">
<h1>Informativa sul trattamento dei dati personali</h1> <h1><%= t("legal.privacy.h1") %></h1>
<p class="legal-meta">Ultimo aggiornamento: <%= legal_last_updated %> · Ai sensi del Regolamento (UE) 2016/679 (GDPR)</p> <p class="legal-meta"><%= raw t("legal.privacy.meta_html", date: legal_last_updated) %></p>
<section> <section>
<h2>1. Titolare del trattamento</h2> <h2><%= t("legal.privacy.s1_title") %></h2>
<p> <p>
Il titolare del trattamento dei dati personali raccolti tramite il sito e i servizi Match Live TV è: <%= t("legal.privacy.s1_intro") %>
</p> </p>
<ul> <ul>
<li><strong><%= MatchLiveTv.privacy_controller_name %></strong></li> <li><strong><%= MatchLiveTv.privacy_controller_name %></strong></li>
<li>Indirizzo: <%= MatchLiveTv.privacy_controller_address %></li> <li><%= t("legal.privacy.s1_label_address") %> <%= 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_email") %> <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a></li>
<% if MatchLiveTv.privacy_controller_vat.present? %> <% 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 %> <% end %>
</ul> </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>
<section> <section>
<h2>2. Ambito di applicazione</h2> <h2><%= t("legal.privacy.s2_title") %></h2>
<p>La presente informativa si applica a:</p> <p><%= t("legal.privacy.s2_intro") %></p>
<ul> <ul>
<li>visitatori del sito web Match Live TV (pagine informative, elenco dirette, registrazione squadra);</li> <li><%= t("legal.privacy.s2_item1") %></li>
<li>utenti registrati (referenti di società sportive, coach, staff invitato);</li> <li><%= t("legal.privacy.s2_item2") %></li>
<li>spettatori che accedono alle dirette tramite link (in genere senza creare un account);</li> <li><%= t("legal.privacy.s2_item3") %></li>
<li>utilizzo dellapp mobile collegata al servizio.</li> <li><%= t("legal.privacy.s2_item4") %></li>
</ul> </ul>
</section> </section>
<section> <section>
<h2>3. Tipologie di dati trattati</h2> <h2><%= t("legal.privacy.s3_title") %></h2>
<p>Possiamo trattare, a seconda delluso del servizio:</p> <p><%= t("legal.privacy.s3_intro") %></p>
<ul> <ul>
<li><strong>Dati identificativi e di contatto:</strong> nome, indirizzo email, credenziali di accesso (password conservata in forma crittografata).</li> <li><%= raw t("legal.privacy.s3_item1_html") %></li>
<li><strong>Dati della squadra:</strong> nome società, sport, partite programmate, avversario, luogo, orari.</li> <li><%= raw t("legal.privacy.s3_item2_html") %></li>
<li><strong>Dati di streaming e tecnici:</strong> stato sessione, punteggio, telemetria di rete/bitrate, log di connessione al servizio video.</li> <li><%= raw t("legal.privacy.s3_item3_html") %></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><%= raw t("legal.privacy.s3_item4_html") %></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><%= raw t("legal.privacy.s3_item5_html") %></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_item6_html") %></li>
</ul> </ul>
</section> </section>
<section> <section>
<h2>4. Trattamento di immagini e dati di atleti minorenni</h2> <h2><%= t("legal.privacy.s4_title") %></h2>
<p> <p>
Match Live TV è destinato anche a competizioni e categorie giovanili (es. Under 14, Under 16, Under 18). <%= raw t("legal.privacy.s4_p1_html") %>
Le dirette possono riprendere <strong>atleti minorenni</strong> identificabili (immagine, voce, numero di maglia, dati di gioco).
</p> </p>
<p><strong>Chi è responsabile del trattamento delle immagini dei minori</strong></p> <p><strong><%= t("legal.privacy.s4_lead1") %></strong></p>
<p> <p>
La <strong>società sportiva</strong> che avvia la trasmissione, in qualità di organizzatrice dellevento sportivo, <%= raw t("legal.privacy.s4_p2_html") %>
è 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.
</p> </p>
<p><strong>Ruolo di Match Live TV</strong></p> <p><strong><%= t("legal.privacy.s4_lead2") %></strong></p>
<p> <p>
Il titolare del servizio fornisce la piattaforma tecnica e, per le trasmissioni effettuate dagli utenti autorizzati, <%= raw t("legal.privacy.s4_p3_html") %>
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).
</p> </p>
<p><strong>Misure richieste alla società sportiva</strong></p> <p><strong><%= t("legal.privacy.s4_lead3") %></strong></p>
<ul> <ul>
<li>informare genitori/tutori sulluso di video in diretta e in archivio;</li> <li><%= t("legal.privacy.s4_item1") %></li>
<li>raccogliere e documentare i consensi necessari prima della diretta;</li> <li><%= t("legal.privacy.s4_item2") %></li>
<li>limitare la diffusione del link a persone autorizzate (familiari, staff);</li> <li><%= t("legal.privacy.s4_item3") %></li>
<li>non pubblicare su canali aperti se non previsto dal consenso o dal regolamento sportivo;</li> <li><%= t("legal.privacy.s4_item4") %></li>
<li>richiedere rimozione di registrazioni quando ne ha diritto linteressato o il tutore.</li> <li><%= t("legal.privacy.s4_item5") %></li>
</ul> </ul>
<p> <p>
Le richieste dei genitori/tutori relative a immagini di minori possono essere inoltrate alla società sportiva <%= raw t(
e, per supporto tecnico o cancellazione da archivio, anche a "legal.privacy.s4_p4_html",
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>. email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}")
) %>
</p> </p>
</section> </section>
<section> <section>
<h2>5. Finalità e basi giuridiche</h2> <h2><%= t("legal.privacy.s5_title") %></h2>
<table class="legal-table"> <table class="legal-table">
<thead> <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> </thead>
<tbody> <tbody>
<tr> <tr>
<td>Registrazione account, gestione squadra e staff</td> <td><%= t("legal.privacy.s5_row1_purpose") %></td>
<td>Esecuzione del contratto / misure precontrattuali</td> <td><%= t("legal.privacy.s5_row1_basis") %></td>
</tr> </tr>
<tr> <tr>
<td>Erogazione dirette, punteggio live, archivio (piani Premium)</td> <td><%= t("legal.privacy.s5_row2_purpose") %></td>
<td>Esecuzione del contratto; legittimo interesse alla sicurezza del servizio</td> <td><%= t("legal.privacy.s5_row2_basis") %></td>
</tr> </tr>
<tr> <tr>
<td>Pagamenti e fatturazione (Stripe)</td> <td><%= t("legal.privacy.s5_row3_purpose") %></td>
<td>Esecuzione del contratto; obblighi di legge</td> <td><%= t("legal.privacy.s5_row3_basis") %></td>
</tr> </tr>
<tr> <tr>
<td>Assistenza, sicurezza, prevenzione abusi</td> <td><%= t("legal.privacy.s5_row4_purpose") %></td>
<td>Legittimo interesse; obblighi di legge</td> <td><%= t("legal.privacy.s5_row4_basis") %></td>
</tr> </tr>
<tr> <tr>
<td>Comunicazioni di servizio (es. reset password)</td> <td><%= t("legal.privacy.s5_row5_purpose") %></td>
<td>Esecuzione del contratto</td> <td><%= t("legal.privacy.s5_row5_basis") %></td>
</tr> </tr>
<tr> <tr>
<td>Marketing diretto (solo se previsto e con consenso ove richiesto)</td> <td><%= t("legal.privacy.s5_row6_purpose") %></td>
<td>Consenso o legittimo interesse, secondo i casi</td> <td><%= t("legal.privacy.s5_row6_basis") %></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</section> </section>
<section> <section>
<h2>6. Destinatari e responsabili del trattamento</h2> <h2><%= t("legal.privacy.s6_title") %></h2>
<p>I dati possono essere comunicati a fornitori che agiscono come responsabili del trattamento, tra cui:</p> <p><%= t("legal.privacy.s6_intro") %></p>
<ul> <ul>
<li>fornitore di hosting e infrastruttura cloud/server;</li> <li><%= t("legal.privacy.s6_item1") %></li>
<li><strong>Stripe</strong> (pagamenti) — <a href="https://stripe.com/it/privacy" rel="noopener" target="_blank">privacy Stripe</a>;</li> <li>
<li>fornitore streaming video (MediaMTX / infrastruttura RTMP-HLS);</li> <%= raw t(
<li>eventuale integrazione <strong>YouTube</strong> se attivata dalla società (soggetto a policy Google);</li> "legal.privacy.s6_item2_html",
<li>fornitore email transazionale (invio link reset password, inviti).</li> 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> </ul>
<p>I dati non sono venduti a terzi per finalità di profilazione commerciale.</p> <p><%= t("legal.privacy.s6_outro") %></p>
</section> </section>
<section> <section>
<h2>7. Trasferimenti extra SEE</h2> <h2><%= t("legal.privacy.s7_title") %></h2>
<p> <p>
Alcuni fornitori (es. Stripe, Google/YouTube) possono trattare dati anche fuori dallo Spazio Economico Europeo. <%= t("legal.privacy.s7_body") %>
In tal caso si applicano garanzie adeguate previste dal GDPR (clausole contrattuali standard, decisioni di adeguatezza).
</p> </p>
</section> </section>
<section> <section>
<h2>8. Conservazione</h2> <h2><%= t("legal.privacy.s8_title") %></h2>
<ul> <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><%= raw t("legal.privacy.s8_item1_html") %></li>
<li><strong>Registrazioni partite (replay):</strong> secondo il piano (es. 30 o 90 giorni), poi cancellazione automatica o su richiesta.</li> <li><%= raw t("legal.privacy.s8_item2_html") %></li>
<li><strong>Log tecnici:</strong> periodo limitato (es. 3090 giorni) salvo obblighi di sicurezza.</li> <li><%= raw t("legal.privacy.s8_item3_html") %></li>
<li><strong>Token reset password:</strong> validità massima <%= MatchLiveTv.password_reset_expiry_hours %> ore.</li> <li><%= raw t("legal.privacy.s8_item4_html", hours: MatchLiveTv.password_reset_expiry_hours) %></li>
</ul> </ul>
</section> </section>
<section> <section>
<h2>9. Diritti degli interessati</h2> <h2><%= t("legal.privacy.s9_title") %></h2>
<p>In qualità di interessato hai diritto di:</p> <p><%= t("legal.privacy.s9_intro") %></p>
<ul> <ul>
<li>accedere ai tuoi dati e ottenerne copia;</li> <li><%= t("legal.privacy.s9_item1") %></li>
<li>rettificarli se inesatti;</li> <li><%= t("legal.privacy.s9_item2") %></li>
<li>chiederne la cancellazione (nei limiti di legge);</li> <li><%= t("legal.privacy.s9_item3") %></li>
<li>limitare o opporti al trattamento, ove applicabile;</li> <li><%= t("legal.privacy.s9_item4") %></li>
<li>revocare il consenso senza pregiudicare la liceità del trattamento basato sul consenso prestato prima della revoca;</li> <li><%= t("legal.privacy.s9_item5") %></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>
<%= raw t(
"legal.privacy.s9_item6_html",
garante_link: link_to("garanteprivacy.it", "https://www.garanteprivacy.it", rel: "noopener", target: "_blank")
) %>
</li>
</ul> </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> <p>
Adottiamo misure tecniche e organizzative adeguate (accesso autenticato, password crittografate, <%= raw t(
comunicazioni HTTPS, segregazione ambienti, limitazione accessi staff). Nessun sistema è invulnerabile: "legal.privacy.s9_outro_html",
se sospetti un accesso non autorizzato, cambia password e contattaci. email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}")
) %>
</p> </p>
</section> </section>
<section> <section>
<h2>11. Cookie e tecnologie simili</h2> <h2><%= t("legal.privacy.s10_title") %></h2>
<p> <p>
Il sito utilizza cookie tecnici necessari (sessione di login, sicurezza, memorizzazione delle preferenze cookie) <%= t("legal.privacy.s10_body") %>
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 %>.
</p> </p>
</section> </section>
<section> <section>
<h2>12. Modifiche</h2> <h2><%= t("legal.privacy.s11_title") %></h2>
<p> <p>
Il titolare può aggiornare questa informativa. La data in alto indica lultima revisione. <%= raw t("legal.privacy.s11_p1_html") %>
In caso di modifiche rilevanti, sarà fornita comunicazione adeguata (es. avviso sul sito o email agli account registrati). </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> </p>
</section> </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> </div>
+60 -66
View File
@@ -1,134 +1,128 @@
<% content_for :title, "Termini di servizio — Match Live TV" %> <% content_for :title, t("legal.terms.title") %>
<% 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 :meta_description, t("legal.terms.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_termini_path) %> <% content_for :canonical_url, seo_absolute_url(public_termini_path) %>
<div class="wrap legal-doc"> <div class="wrap legal-doc">
<h1>Termini e condizioni di servizio</h1> <h1><%= t("legal.terms.h1") %></h1>
<p class="legal-meta">Ultimo aggiornamento: <%= legal_last_updated %></p> <p class="legal-meta"><%= t("legal.terms.meta", date: legal_last_updated) %></p>
<section> <section>
<h2>1. Oggetto</h2> <h2><%= t("legal.terms.s1_title") %></h2>
<p> <p>
I presenti Termini regolano luso della piattaforma <strong>Match Live TV</strong> (sito web, applicazione mobile <%= raw t("legal.terms.s1_p1_html") %>
e servizi connessi) per la gestione di dirette sportive dilettantistiche/giovanili, punteggio live,
condivisione link agli spettatori e funzioni Premium (archivio, YouTube, ecc.).
</p> </p>
<p> <p>
Il servizio è erogato da <strong><%= MatchLiveTv.privacy_controller_name %></strong> <%= raw t(
(di seguito «Fornitore»). Registrandoti o utilizzando il servizio accetti integralmente questi Termini "legal.terms.s1_p2_html",
e l<%= link_to "informativa privacy", public_privacy_path %>. provider_name: MatchLiveTv.privacy_controller_name,
privacy_link: link_to(t("legal.terms.s1_privacy_link_text"), public_privacy_path)
) %>
</p> </p>
</section> </section>
<section> <section>
<h2>2. Chi può usare il servizio</h2> <h2><%= t("legal.terms.s2_title") %></h2>
<ul> <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><%= raw t("legal.terms.s2_item1_html") %></li>
<li>I responsabili trasmissione invitati usano credenziali personali e si impegnano a custodirle.</li> <li><%= t("legal.terms.s2_item2") %></li>
<li>Gli spettatori che aprono il link della diretta di solito non necessitano di account.</li> <li><%= t("legal.terms.s2_item3") %></li>
</ul> </ul>
</section> </section>
<section> <section>
<h2>3. Account, piani e pagamenti</h2> <h2><%= t("legal.terms.s3_title") %></h2>
<p> <p>
Sono disponibili piani Free e Premium (Light / Full) con limiti su staff, dirette concorrenti, archivio e integrazioni. <%= raw t(
I prezzi sono indicati nella pagina <%= link_to "Prezzi", public_prezzi_path %>. "legal.terms.s3_p1_html",
pricing_link: link_to(t("legal.terms.s3_pricing_link_text"), public_prezzi_path)
) %>
</p> </p>
<p> <p>
I pagamenti dei piani a pagamento sono gestiti da <strong>Stripe</strong>. Accettando il checkout, <%= raw t("legal.terms.s3_p2_html") %>
accetti anche i termini di Stripe per il pagamento. Il Fornitore non memorizza i dati completi della carta.
</p> </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>
<section> <section>
<h2>4. Dirette, contenuti e protezione dei minori</h2> <h2><%= t("legal.terms.s4_title") %></h2>
<p><strong>Responsabilità della società sportiva</strong></p> <p><strong><%= t("legal.terms.s4_lead1") %></strong></p>
<p> <p>
La società che avvia una diretta è responsabile dei contenuti trasmessi (immagini, audio, commenti) <%= raw t("legal.terms.s4_p1_html") %>
e della conformità alle leggi, al regolamento federale e ai consensi necessari, in particolare quando
sono ripresi <strong>atleti minorenni</strong>.
</p> </p>
<p>La società si impegna a:</p> <p><%= t("legal.terms.s4_p2") %></p>
<ul> <ul>
<li>ottenere e conservare i consensi genitoriali/tutori o altra base giuridica prima della ripresa;</li> <li><%= t("legal.terms.s4_item1") %></li>
<li>informare genitori e atleti su dove e per quanto tempo è visibile la diretta/archivio;</li> <li><%= t("legal.terms.s4_item2") %></li>
<li>non trasmettere contenuti illegali, diffamatori, violenti o non pertinenti allevento sportivo;</li> <li><%= t("legal.terms.s4_item3") %></li>
<li>rispettare il diritto allimmagine e alla voce di tutte le persone inquadrate.</li> <li><%= t("legal.terms.s4_item4") %></li>
</ul> </ul>
<p> <p>
Il Fornitore può sospendere o interrompere trasmissioni e account in caso di segnalazioni fondate <%= t("legal.terms.s4_p3") %>
o violazioni gravi, senza pregiudizio di azioni legali.
</p> </p>
</section> </section>
<section> <section>
<h2>5. Uso consentito e divieti</h2> <h2><%= t("legal.terms.s5_title") %></h2>
<p>È vietato:</p> <p><%= t("legal.terms.s5_intro") %></p>
<ul> <ul>
<li>usare il servizio per finalità non sportive o commerciali non autorizzate;</li> <li><%= t("legal.terms.s5_item1") %></li>
<li>aggirare limiti tecnici o di piano (staff, dirette concorrenti);</li> <li><%= t("legal.terms.s5_item2") %></li>
<li>condividere credenziali o pubblicare link su canali che espongono minori a rischi non previsti dal consenso;</li> <li><%= t("legal.terms.s5_item3") %></li>
<li>caricare malware, effettuare attacchi o interferire con linfrastruttura;</li> <li><%= t("legal.terms.s5_item4") %></li>
<li>rivendere il servizio senza accordo scritto con il Fornitore.</li> <li><%= t("legal.terms.s5_item5") %></li>
</ul> </ul>
</section> </section>
<section> <section>
<h2>6. Proprietà intellettuale</h2> <h2><%= t("legal.terms.s6_title") %></h2>
<p> <p>
Marchi, software, design e documentazione del servizio restano del Fornitore o dei suoi licenzianti. <%= t("legal.terms.s6_body") %>
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.
</p> </p>
</section> </section>
<section> <section>
<h2>7. Disponibilità e limitazioni</h2> <h2><%= t("legal.terms.s7_title") %></h2>
<p> <p>
Il servizio dipende da reti mobili, hardware e fornitori terzi. Non garantiamo assenza assoluta di interruzioni, <%= t("legal.terms.s7_body") %>
ritardi o perdita di dati, pur adottando misure ragionevoli. La qualità video può variare in base alla connessione in palestra.
</p> </p>
</section> </section>
<section> <section>
<h2>8. Limitazione di responsabilità</h2> <h2><%= t("legal.terms.s8_title") %></h2>
<p> <p>
Nei limiti consentiti dalla legge italiana, il Fornitore non risponde di danni indiretti, perdita di profitto, <%= t("legal.terms.s8_p1") %>
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.
</p> </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>
<section> <section>
<h2>9. Recesso, sospensione e cancellazione</h2> <h2><%= t("legal.terms.s9_title") %></h2>
<p> <p>
Puoi richiedere la chiusura dellaccount scrivendo a <%= raw t(
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>. "legal.terms.s9_body_html",
Il Fornitore può sospendere laccount per inadempienza, mancato pagamento o violazione dei Termini. email_link: link_to(MatchLiveTv.privacy_controller_email, "mailto:#{MatchLiveTv.privacy_controller_email}")
Alla cessazione, i dati sono trattati come indicato in privacy (conservazione limitata o cancellazione). ) %>
</p> </p>
</section> </section>
<section> <section>
<h2>10. Legge applicabile e foro</h2> <h2><%= t("legal.terms.s10_title") %></h2>
<p> <p>
I Termini sono regolati dalla <strong>legge italiana</strong>. Per controversie con consumatori si applicano <%= raw t("legal.terms.s10_body_html") %>
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.
</p> </p>
</section> </section>
<section> <section>
<h2>11. Contatti</h2> <h2><%= t("legal.terms.s11_title") %></h2>
<p> <p>
<%= MatchLiveTv.privacy_controller_name %><br> <%= raw t(
<%= MatchLiveTv.privacy_controller_address %><br> "legal.terms.s11_body_html",
Email: <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a> 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> </p>
</section> </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> </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" %> <% content_for :robots, "noindex, nofollow" %>
<section class="auth-page"> <section class="auth-page">
<h1>Scegli una nuova password</h1> <h1><%= t("auth.password_reset.title") %></h1>
<div class="card"> <div class="card">
<%= form_with url: public_password_reset_path, method: :patch, local: true do %> <%= form_with url: public_password_reset_path, method: :patch, local: true do %>
<%= hidden_field_tag :token, @token %> <%= hidden_field_tag :token, @token %>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: :password, name: :password,
label: "Nuova password (min. 8 caratteri)", label: t("auth.password_reset.new_password_label"),
input_type: "text", input_type: "text",
required: true, required: true,
minlength: 8, minlength: 8,
autocomplete: "new-password" %> autocomplete: "new-password" %>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: :password_confirmation, name: :password_confirmation,
label: "Conferma password", label: t("auth.password_confirmation"),
input_type: "text", input_type: "text",
required: true, required: true,
autocomplete: "new-password" %> autocomplete: "new-password" %>
<%= submit_tag "Salva password", class: "btn btn-primary" %> <%= submit_tag t("auth.password_reset.submit"), class: "btn btn-primary" %>
<% end %> <% 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> </div>
</section> </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" %> <% content_for :robots, "noindex, nofollow" %>
<section class="auth-page"> <section class="auth-page">
<h1>Password dimenticata</h1> <h1><%= t("auth.password_forgot.title") %></h1>
<p class="auth-lead">Inserisci lemail dellaccount: ti invieremo un link per reimpostare la password.</p> <p class="auth-lead"><%= t("auth.password_forgot.lead") %></p>
<div class="card"> <div class="card">
<%= form_with url: public_password_forgot_path, method: :post, local: true do %> <%= 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" %> <%= 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 %> <% 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> </div>
</section> </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> <button type="button" class="regia-btn regia-btn--minus" data-action="away_undo" style="width:100%;margin-top:4px"></button>
</div> </div>
</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-score-line" id="score-line"><%= live_score_points_label(@match, @score) %></p>
<p class="regia-partials" id="partials-line" hidden></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> <button type="button" class="regia-btn regia-btn--minus" data-action="away_undo" style="width:100%;margin-top:6px"></button>
</div> </div>
</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 || {} %> <% 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-score-line" id="score-line" hidden></p>
<p class="regia-partials" id="partials-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> <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"> <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_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">Reset</button> <button type="button" class="regia-btn regia-btn--outline" data-action="clock_reset"><%= t("regia.board.reset") %></button>
</div> </div>
@@ -1,7 +1,7 @@
<p class="regia-sets" id="sets-line"><%= live_score_sets_label(@score) || "—" %></p> <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-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? %>> <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> </p>
<div class="regia-team-row"> <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> <button type="button" class="regia-btn regia-btn--minus" data-action="home_undo" style="width:100%;margin-top:6px"></button>
</div> </div>
<div class="regia-center-hint" id="center-hint"> <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> <div>
<div class="regia-team-name"><%= @match.opponent_name %></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> <button type="button" class="regia-btn regia-btn--minus" data-action="away_undo" style="width:100%;margin-top:6px"></button>
</div> </div>
</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 :robots, "noindex, nofollow" %>
<% content_for :head do %> <% content_for :head do %>
@@ -8,6 +8,7 @@
<% end %> <% end %>
<div class="regia-page" id="regia-app" <div class="regia-page" id="regia-app"
data-i18n="<%= j regia_js_i18n_json %>"
data-token="<%= j params[:token] %>" data-token="<%= j params[:token] %>"
data-board="<%= @match.effective_board_type %>" data-board="<%= @match.effective_board_type %>"
data-status-url="<%= j public_regia_status_path(params[:token]) %>" data-status-url="<%= j public_regia_status_path(params[:token]) %>"
@@ -29,34 +30,34 @@
<header class="regia-header"> <header class="regia-header">
<h1><%= @team.name %> vs <%= @match.opponent_name %></h1> <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')) %>"> <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> </span>
</header> </header>
<% if @live_share_url.present? %> <% if @live_share_url.present? %>
<div class="regia-share-group"> <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"> <div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-live" style="margin:0"> <button type="button" class="regia-btn regia-btn--outline" id="btn-share-live" style="margin:0">
Condividi diretta <%= t("regia.share_live") %>
</button> </button>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-live" style="margin:0"> <button type="button" class="regia-btn regia-btn--outline" id="btn-copy-live" style="margin:0">
Copia link <%= t("regia.copy_link") %>
</button> </button>
</div> </div>
</div> </div>
<% end %> <% end %>
<div class="regia-share-group"> <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"> <div class="regia-share">
<button type="button" class="regia-btn regia-btn--outline" id="btn-share-regia" style="margin:0"> <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>
<button type="button" class="regia-btn regia-btn--outline" id="btn-copy-regia" style="margin:0"> <button type="button" class="regia-btn regia-btn--outline" id="btn-copy-regia" style="margin:0">
Copia link <%= t("regia.copy_link") %>
</button> </button>
</div> </div>
</div> </div>
@@ -66,7 +67,7 @@
<video id="regia-preview" playsinline muted autoplay></video> <video id="regia-preview" playsinline muted autoplay></video>
<% end %> <% end %>
<div id="regia-preview-placeholder" class="regia-preview__placeholder"> <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>
</div> </div>
@@ -75,8 +76,8 @@
</section> </section>
<% unless @stream_closed %> <% 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--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">Chiudi diretta</button> <button type="button" class="regia-btn regia-btn--danger" id="btn-stop"><%= t("regia.stop") %></button>
<% end %> <% end %>
</div> </div>
@@ -84,11 +85,11 @@
<div id="regia-modal" class="regia-modal" aria-hidden="true"> <div id="regia-modal" class="regia-modal" aria-hidden="true">
<div class="regia-modal__card"> <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> <p id="modal-body"></p>
<div class="regia-modal__actions"> <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--yellow" id="modal-confirm"><%= t("regia.modal.confirm") %></button>
<button type="button" class="regia-btn regia-btn--outline" id="modal-cancel">Annulla</button> <button type="button" class="regia-btn regia-btn--outline" id="modal-cancel"><%= t("regia.modal.cancel") %></button>
</div> </div>
</div> </div>
</div> </div>
@@ -1,18 +1,18 @@
<% content_for :title, "Registra la squadra — Match Live TV" %> <% content_for :title, t("auth.signup.meta_title") %>
<% 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 :meta_description, t("auth.signup.meta_description") %>
<% content_for :robots, "noindex, follow" %> <% content_for :robots, "noindex, follow" %>
<section class="auth-page"> <section class="auth-page">
<h1>Registra la squadra</h1> <h1><%= t("auth.signup.title") %></h1>
<p class="auth-lead">Crea l'account del referente (coach o dirigente), poi configurerai la squadra.</p> <p class="auth-lead"><%= t("auth.signup.lead") %></p>
<div class="card"> <div class="card">
<%= form_with model: @user, url: public_signup_path do |f| %> <%= 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 %> <%= f.text_field :name, required: true %>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: "user[email]", name: "user[email]",
id: "user_email", id: "user_email",
label: "Email", label: t("auth.email"),
value: @user.email, value: @user.email,
input_type: "email", input_type: "email",
required: true, required: true,
@@ -20,7 +20,7 @@
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: "user[password]", name: "user[password]",
id: "user_password", id: "user_password",
label: "Password", label: t("auth.password"),
input_type: "text", input_type: "text",
required: true, required: true,
minlength: 8, minlength: 8,
@@ -28,24 +28,25 @@
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: "user[password_confirmation]", name: "user[password_confirmation]",
id: "user_password_confirmation", id: "user_password_confirmation",
label: "Conferma password", label: t("auth.password_confirmation"),
input_type: "text", input_type: "text",
required: true, required: true,
autocomplete: "new-password" %> autocomplete: "new-password" %>
<label class="legal-accept"> <label class="legal-accept">
<%= check_box_tag :accept_terms, "1", false, required: true %> <%= check_box_tag :accept_terms, "1", false, required: true %>
<span class="legal-accept-text"> <span class="legal-accept-text">
Ho letto e accetto l<%= link_to "informativa privacy", public_privacy_path, target: "_blank", rel: "noopener" %> <%= raw t(
e i <%= link_to "termini di servizio", public_termini_path, target: "_blank", rel: "noopener" %>. "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> </span>
</label> </label>
<p class="legal-accept-hint"> <p class="legal-accept-hint">
Registrandoti dichiari di essere maggiorenne e, se operi per una società sportiva con atleti minorenni, <%= t("auth.signup.legal_hint") %>
di avere legittima facoltà (es. dirigente, coach autorizzato) e di rispettare le regole su immagini dei minori
descritte in privacy.
</p> </p>
<%= f.submit "Continua", class: "btn btn-primary" %> <%= f.submit t("auth.signup.submit"), class: "btn btn-primary" %>
<% end %> <% 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> </div>
</section> </section>
+18 -18
View File
@@ -1,11 +1,11 @@
<% content_for :title, "Replay — Match Live TV" %> <% content_for :title, t("replay.index.meta_title") %>
<% content_for :meta_description, "Archivio replay delle dirette sportive su Match Live TV. Cerca per società, squadra o avversario." %> <% content_for :meta_description, t("replay.index.meta_description") %>
<div class="wrap replay-index"> <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> <h1><%= t("replay.index.title") %></h1>
<p class="results-hint">Replay pubblici delle società sportive — riguarda le partite già trasmesse.</p> <p class="results-hint"><%= t("replay.index.hint") %></p>
<%= form_with url: public_replay_index_path, method: :get, local: true, class: "search-form" do %> <%= form_with url: public_replay_index_path, method: :get, local: true, class: "search-form" do %>
<% if params[:club_id].present? %> <% if params[:club_id].present? %>
@@ -15,13 +15,13 @@
type="search" type="search"
name="q" name="q"
value="<%= @query %>" value="<%= @query %>"
placeholder="Es. Tigers Volley, avversario, società, luogo…" placeholder="<%= t("replay.index.search_placeholder") %>"
aria-label="Cerca replay" aria-label="<%= t("replay.index.search_aria_label") %>"
autocomplete="off" 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? %> <% 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 %>
<% end %> <% end %>
@@ -30,9 +30,9 @@
<% if @query.present? %> <% if @query.present? %>
<%= hidden_field_tag :q, @query %> <%= hidden_field_tag :q, @query %>
<% end %> <% 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()"> <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| %> <% @clubs.each do |club| %>
<option value="<%= club.id %>"<%= " selected" if params[:club_id].to_s == club.id.to_s %>><%= club.name %></option> <option value="<%= club.id %>"<%= " selected" if params[:club_id].to_s == club.id.to_s %>><%= club.name %></option>
<% end %> <% end %>
@@ -41,9 +41,9 @@
<% end %> <% end %>
<% if @query.present? %> <% if @query.present? %>
<% club_suffix = @filter_club ? t("replay.index.results_hint_club_suffix", name: @filter_club.name) : "" %>
<p class="results-hint"> <p class="results-hint">
Risultati per «<%= h @query %>»<% if @filter_club %> · <%= @filter_club.name %><% end %>: <%= t("replay.index.results_hint", query: @query, club_suffix: club_suffix, count: @recordings.size) %>
<%= @recordings.size %> replay
</p> </p>
<% end %> <% end %>
@@ -81,12 +81,12 @@
<% else %> <% else %>
<div class="empty-state"> <div class="empty-state">
<% if @query.present? || params[:club_id].present? %> <% if @query.present? || params[:club_id].present? %>
<p><strong>Nessun replay trovato</strong> con i filtri selezionati.</p> <p><strong><%= t("replay.index.empty_filtered_title") %></strong><%= t("replay.index.empty_filtered_suffix") %></p>
<p><%= link_to "Mostra tutti i replay pubblici", public_replay_index_path, class: "btn btn-secondary" %></p> <p><%= link_to t("replay.index.empty_filtered_show_all"), public_replay_index_path, class: "btn btn-secondary" %></p>
<% else %> <% else %>
<p><strong>Nessun replay pubblico al momento.</strong></p> <p><strong><%= t("replay.index.empty_default_title") %></strong></p>
<p>Quando una società rende pubblica una registrazione, comparirà qui.</p> <p><%= t("replay.index.empty_default_body") %></p>
<%= link_to "Vai alle dirette live", public_live_index_path, class: "btn btn-secondary", style: "margin-top:12px;display:inline-block" %> <%= 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 %> <% end %>
</div> </div>
<% end %> <% end %>
+21 -21
View File
@@ -1,18 +1,18 @@
<% club_name = @match.team.club&.name %> <% club_name = @match.team.club&.name %>
<% content_for :title, "Replay#{@recording.title_or_default}" %> <% content_for :title, t("replay.show.title", title: @recording.title_or_default) %>
<% content_for :meta_description, "Replay #{@recording.title_or_default} su Match Live TV." %> <% content_for :meta_description, t("replay.show.meta_description", title: @recording.title_or_default) %>
<% content_for :robots, @recording.publicly_listed? ? "index, follow" : "noindex, nofollow" %> <% content_for :robots, @recording.publicly_listed? ? "index, follow" : "noindex, nofollow" %>
<div class="wrap"> <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) %> <%= live_match_page_heading(@match) %>
<% if @recording.status == "processing" %> <% if @recording.status == "processing" %>
<div class="stream-ended" role="status"> <div class="stream-ended" role="status">
<div class="stream-ended-icon" aria-hidden="true"></div> <div class="stream-ended-icon" aria-hidden="true"></div>
<h2>Replay in elaborazione</h2> <h2><%= t("replay.show.processing_title") %></h2>
<p>La registrazione viene preparata. Riceverai unemail quando sarà pronta.</p> <p><%= t("replay.show.processing_body") %></p>
</div> </div>
<% elsif @recording.ready? && @recording.storage_key.present? %> <% elsif @recording.ready? && @recording.storage_key.present? %>
<div class="live-player-wrap"> <div class="live-player-wrap">
@@ -25,18 +25,18 @@
session: @session, session: @session,
stream_closed: true, stream_closed: true,
on_air: false, on_air: false,
badge_label: "REPLAY", badge_label: t("replay.show.badge_label"),
badge_class: "badge-ended" %> badge_class: "badge-ended" %>
</div> </div>
<p class="replay-show__meta-line"> <p class="replay-show__meta-line">
<%= l_local(@recording.recorded_at_or_fallback) %> <%= l_local(@recording.recorded_at_or_fallback) %>
· Durata <%= @recording.duration_label %> · <%= t("replay.show.meta_line_duration", value: @recording.duration_label) %>
· <%= @recording.views_label %> · <%= @recording.views_label %>
<% if @recording.expires_at %> <% if @recording.expires_at %>
· Fino al <%= l_local(@recording.expires_at) %> · <%= t("replay.show.meta_line_until", date: l_local(@recording.expires_at)) %>
<% end %> <% 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 != "—" %> <% if @recording.source_platform_label != "—" %>
· <%= @recording.source_platform_label %> · <%= @recording.source_platform_label %>
<% end %> <% end %>
@@ -46,10 +46,10 @@
<% if (ent.phone_download_enabled? && (logged_in? || @recording.unlisted?)) || @recording.youtube_watch_url %> <% if (ent.phone_download_enabled? && (logged_in? || @recording.unlisted?)) || @recording.youtube_watch_url %>
<div class="replay-show__actions"> <div class="replay-show__actions">
<% if ent.phone_download_enabled? && (logged_in? || @recording.unlisted?) %> <% 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 %> <% end %>
<% if @recording.youtube_watch_url %> <% 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 %> <% end %>
</div> </div>
<% end %> <% end %>
@@ -65,28 +65,28 @@
session: @session, session: @session,
stream_closed: true, stream_closed: true,
on_air: false, on_air: false,
badge_label: "REPLAY", badge_label: t("replay.show.badge_label"),
badge_class: "badge-ended" %> badge_class: "badge-ended" %>
</div> </div>
<p class="replay-show__meta-line"> <p class="replay-show__meta-line">
Replay su YouTube (copia server non presente). <%= t("replay.show.youtube_only_body") %>
<%= link_to "Apri su YouTube", @recording.youtube_watch_url, target: "_blank", rel: "noopener" %> <%= link_to t("replay.show.youtube_link"), @recording.youtube_watch_url, target: "_blank", rel: "noopener" %>
</p> </p>
<% elsif @recording.ready? %> <% elsif @recording.ready? %>
<div class="stream-ended" role="status"> <div class="stream-ended" role="status">
<h2>File non disponibile</h2> <h2><%= t("replay.show.file_missing_title") %></h2>
<p>Il video non è più sul server (archivio rimosso o in migrazione).</p> <p><%= t("replay.show.file_missing_body") %></p>
<p class="stream-ended-meta">Durata registrata: <%= @recording.duration_label %> · <%= @recording.byte_size_label %></p> <p class="stream-ended-meta"><%= t("replay.show.file_missing_meta", duration: @recording.duration_label, size: @recording.byte_size_label) %></p>
</div> </div>
<% elsif @recording.status == "failed" %> <% elsif @recording.status == "failed" %>
<div class="stream-ended" role="status"> <div class="stream-ended" role="status">
<h2>Replay non disponibile</h2> <h2><%= t("replay.show.failed_title") %></h2>
<p><%= @recording.error_message.presence || "Errore di elaborazione della registrazione." %></p> <p><%= @recording.error_message.presence || t("replay.show.failed_default_body") %></p>
</div> </div>
<% else %> <% else %>
<div class="stream-ended" role="status"> <div class="stream-ended" role="status">
<h2>Replay non disponibile</h2> <h2><%= t("replay.show.unavailable_title") %></h2>
<p>Questa registrazione non è più accessibile.</p> <p><%= t("replay.show.unavailable_body") %></p>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -1,26 +1,26 @@
<% content_for :title, "Accedi — Match Live TV" %> <% content_for :title, t("auth.login.meta_title") %>
<% content_for :meta_description, "Accesso area riservata per staff e società registrate su Match Live TV." %> <% content_for :meta_description, t("auth.login.meta_description") %>
<% content_for :robots, "noindex, nofollow" %> <% content_for :robots, "noindex, nofollow" %>
<section class="auth-page"> <section class="auth-page">
<h1>Accedi</h1> <h1><%= t("auth.login.title") %></h1>
<div class="card"> <div class="card">
<%= form_with url: public_login_path do |f| %> <%= form_with url: public_login_path do |f| %>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: :email, name: :email,
label: "Email", label: t("auth.email"),
value: params[:email], value: params[:email],
input_type: "email", input_type: "email",
required: true, required: true,
autocomplete: "username" %> autocomplete: "username" %>
<%= render "shared/input_toggle", <%= render "shared/input_toggle",
name: :password, name: :password,
label: "Password", label: t("auth.password"),
required: true, required: true,
autocomplete: "current-password" %> autocomplete: "current-password" %>
<p class="auth-forgot"><%= link_to "Password dimenticata?", public_password_forgot_path %></p> <p class="auth-forgot"><%= link_to t("auth.login.forgot_password"), public_password_forgot_path %></p>
<%= submit_tag "Entra", class: "btn btn-primary" %> <%= submit_tag t("auth.login.submit"), class: "btn btn-primary" %>
<% end %> <% 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> </div>
</section> </section>
@@ -1,12 +1,11 @@
<% content_for :title, "Squadre su Match Live TV — Dirette e replay sport giovanili" %> <% content_for :title, t("team_pages.index.meta_title") %>
<% 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 :meta_description, t("team_pages.index.meta_description") %>
<% content_for :canonical_url, seo_absolute_url(public_team_pages_path(filter_params_for_canonical)) %> <% content_for :canonical_url, seo_absolute_url(public_team_pages_path(filter_params_for_canonical)) %>
<div class="wrap team-directory"> <div class="wrap team-directory">
<h1>Squadre su Match Live TV</h1> <h1><%= t("team_pages.index.heading") %></h1>
<p class="results-hint"> <p class="results-hint">
Società e squadre con dirette, replay pubblici o partite in programma. <%= t("team_pages.index.hint") %>
Cerca la tua squadra e apri la pagina per seguire calendario e archivio.
</p> </p>
<%= form_with url: public_team_pages_path, method: :get, local: true, class: "team-directory-filters" do %> <%= form_with url: public_team_pages_path, method: :get, local: true, class: "team-directory-filters" do %>
@@ -15,18 +14,18 @@
type="search" type="search"
name="q" name="q"
value="<%= @query %>" value="<%= @query %>"
placeholder="Nome squadra, società, città o palestra…" placeholder="<%= t("team_pages.index.search_placeholder") %>"
aria-label="Cerca squadra" aria-label="<%= t("team_pages.index.search_aria_label") %>"
autocomplete="off" 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>
<div class="team-directory-filters__row"> <div class="team-directory-filters__row">
<div class="team-directory-filters__field"> <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()"> <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| %> <% @sport_options.each do |sport| %>
<option value="<%= sport[:key] %>"<%= " selected" if @sport == sport[:key] %>><%= sport[:label] %></option> <option value="<%= sport[:key] %>"<%= " selected" if @sport == sport[:key] %>><%= sport[:label] %></option>
<% end %> <% end %>
@@ -34,20 +33,20 @@
</div> </div>
<div class="team-directory-filters__field team-directory-filters__field--chips"> <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"> <div class="team-directory-chips" role="group" aria-labelledby="team-filter-chips-label">
<label class="filter-chip<%= " is-active" if @live_filter %>"> <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()" %> <%= 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__face" aria-hidden="true">
<span class="filter-chip__dot filter-chip__dot--live"></span> <span class="filter-chip__dot filter-chip__dot--live"></span>
In diretta <%= t("team_pages.index.chip_live") %>
</span> </span>
</label> </label>
<label class="filter-chip<%= " is-active" if @replays_filter %>"> <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()" %> <%= 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__face" aria-hidden="true">
<span class="filter-chip__icon"></span> <span class="filter-chip__icon"></span>
Con replay <%= t("team_pages.index.chip_replays") %>
</span> </span>
</label> </label>
</div> </div>
@@ -56,7 +55,7 @@
<% if @query.present? || @sport.present? || @live_filter || @replays_filter %> <% if @query.present? || @sport.present? || @live_filter || @replays_filter %>
<div class="team-directory-filters__actions"> <div class="team-directory-filters__actions">
<span class="team-directory-filters__label team-directory-filters__label--spacer" aria-hidden="true">&nbsp;</span> <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> </div>
<% end %> <% end %>
</div> </div>
@@ -64,12 +63,12 @@
<% if @query.present? || @sport.present? || @live_filter || @replays_filter %> <% if @query.present? || @sport.present? || @live_filter || @replays_filter %>
<p class="results-hint"> <p class="results-hint">
<%= @entries.size %> squadre trovate <%= t("team_pages.index.results_found", count: @entries.size) %>
<% if @query.present? %> per «<%= h @query %>»<% end %> <% if @query.present? %><%= t("team_pages.index.results_query_suffix", query: @query) %><% end %>
<% if @sport.present? %> · <%= Sports::Catalog.find_optional(@sport)&.dig(:label) || @sport %><% end %> <% if @sport.present? %><%= t("team_pages.index.results_sport_suffix", sport: Sports::Catalog.find_optional(@sport)&.dig(:label) || @sport) %><% end %>
</p> </p>
<% else %> <% 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 %> <% end %>
<% if @entries.any? %> <% if @entries.any? %>
@@ -96,13 +95,13 @@
<p class="team-directory-card__meta"><%= team.sport_label %></p> <p class="team-directory-card__meta"><%= team.sport_label %></p>
<div class="team-directory-card__badges"> <div class="team-directory-card__badges">
<% if entry.live_now %> <% 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 %> <% end %>
<% if entry.upcoming_count.positive? %> <% 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 %> <% end %>
<% if entry.replay_count.positive? %> <% 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 %> <% end %>
</div> </div>
</div> </div>
@@ -112,18 +111,18 @@
<% else %> <% else %>
<div class="empty-state"> <div class="empty-state">
<% if @query.present? || @sport.present? || @live_filter || @replays_filter %> <% if @query.present? || @sport.present? || @live_filter || @replays_filter %>
<p><strong>Nessuna squadra trovata</strong> con i filtri selezionati.</p> <p><strong><%= t("team_pages.index.empty_filtered_title") %></strong><%= t("team_pages.index.empty_filtered_suffix") %></p>
<p><%= link_to "Mostra tutte le squadre attive", public_team_pages_path, class: "btn btn-secondary" %></p> <p><%= link_to t("team_pages.index.empty_filtered_show_all"), public_team_pages_path, class: "btn btn-secondary" %></p>
<% else %> <% else %>
<p><strong>Nessuna squadra attiva al momento.</strong></p> <p><strong><%= t("team_pages.index.empty_default_title") %></strong></p>
<p>Quando una società avvierà dirette o pubblicherà replay, comparirà qui.</p> <p><%= t("team_pages.index.empty_default_body") %></p>
<%= link_to "Vai alle dirette live", public_live_index_path, class: "btn btn-secondary", style: "margin-top:12px;display:inline-block" %> <%= 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 %> <% end %>
</div> </div>
<% end %> <% end %>
<p class="team-directory-footer muted"> <p class="team-directory-footer muted">
<%= link_to "Dirette in corso", public_live_index_path %> · <%= link_to t("team_pages.index.footer_live_link"), public_live_index_path %> ·
<%= link_to "Archivio replay", public_replay_index_path %> <%= link_to t("team_pages.index.footer_replay_link"), public_replay_index_path %>
</p> </p>
</div> </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 :meta_description, team_page_meta_description(@team, @club) %>
<% content_for :canonical_url, seo_absolute_url(public_team_page_path(@team.slug)) %> <% content_for :canonical_url, seo_absolute_url(public_team_page_path(@team.slug)) %>
<% content_for :head do %> <% content_for :head do %>
@@ -9,7 +9,7 @@
<div class="wrap team-public-page"> <div class="wrap team-public-page">
<nav class="team-dashboard-nav" aria-label="Percorso"> <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> </nav>
<header class="roster-hero card team-public-hero" style="--club-primary:<%= @team.effective_primary_color %>;--club-secondary:<%= @team.effective_secondary_color %>"> <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"> <p class="team-public-meta">
<%= @team.sport_label %> <%= @team.sport_label %>
<% if @live_sessions.any? %> <% 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 %> <% end %>
</p> </p>
<% if @team.description.present? %> <% if @team.description.present? %>
@@ -27,11 +27,11 @@
<% end %> <% end %>
<div class="team-public-actions"> <div class="team-public-actions">
<% if @live_sessions.any? %> <% 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 %> <% 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? %> <% 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 %> <% end %>
</div> </div>
</div> </div>
@@ -51,7 +51,7 @@
<% if @live_sessions.any? %> <% if @live_sessions.any? %>
<section class="team-public-section" aria-labelledby="team-live-heading"> <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"> <div class="live-grid">
<% @live_sessions.each do |session| %> <% @live_sessions.each do |session| %>
<% match = session.match %> <% match = session.match %>
@@ -66,21 +66,21 @@
<p class="card-score"> <p class="card-score">
<span class="card-sets"><%= live_score_sets_label(session.score_state, match) %></span> <span class="card-sets"><%= live_score_sets_label(session.score_state, match) %></span>
<% if live_score_partials_label(session.score_state).present? %> <% 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 %> <% end %>
<span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span> <span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span>
</p> </p>
<% end %> <% end %>
<div class="badges"> <div class="badges">
<% if on_air %> <% 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? %> <% elsif session.paused? %>
<span class="badge badge-connecting">In pausa</span> <span class="badge badge-connecting"><%= t("live.index.badge_paused") %></span>
<% else %> <% else %>
<span class="badge badge-live">Live</span> <span class="badge badge-live"><%= t("live.index.badge_live") %></span>
<% end %> <% end %>
</div> </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> </article>
<% end %> <% end %>
</div> </div>
@@ -89,8 +89,8 @@
<% if @upcoming_matches.any? %> <% if @upcoming_matches.any? %>
<section class="team-public-section" aria-labelledby="team-upcoming-heading"> <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> <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">Programmate dal club: la diretta partirà quando lo staff avvierà la trasmissione.</p> <p class="results-hint"><%= t("team_pages.show.upcoming_hint") %></p>
<div class="live-grid upcoming-grid"> <div class="live-grid upcoming-grid">
<% @upcoming_matches.each do |match| %> <% @upcoming_matches.each do |match| %>
<article class="live-card live-card--upcoming"> <article class="live-card live-card--upcoming">
@@ -103,7 +103,7 @@
</p> </p>
<p class="upcoming-when"><%= live_scheduled_relative(match.scheduled_at) %></p> <p class="upcoming-when"><%= live_scheduled_relative(match.scheduled_at) %></p>
<div class="badges"> <div class="badges">
<span class="badge badge-scheduled">In programma</span> <span class="badge badge-scheduled"><%= t("live.index.badge_scheduled") %></span>
</div> </div>
</article> </article>
<% end %> <% end %>
@@ -113,7 +113,7 @@
<% if @recordings.any? %> <% if @recordings.any? %>
<section class="team-public-section" aria-labelledby="team-replays-heading"> <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"> <div class="replay-grid">
<% @recordings.each do |rec| %> <% @recordings.each do |rec| %>
<% match = rec.stream_session.match %> <% match = rec.stream_session.match %>
@@ -137,21 +137,21 @@
<% end %> <% end %>
</div> </div>
<p class="team-public-more"> <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> </p>
</section> </section>
<% end %> <% end %>
<% if @roster_by_category %> <% if @roster_by_category %>
<section class="team-public-section" aria-labelledby="team-roster-heading"> <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"> <div class="team-public-roster">
<% TeamRosterMember::DISPLAY_ORDER.each do |category| %> <% TeamRosterMember::DISPLAY_ORDER.each do |category| %>
<% members = @roster_by_category[category] %> <% members = @roster_by_category[category] %>
<% next if members.blank? %> <% next if members.blank? %>
<section class="roster-section card"> <section class="roster-section card">
<header class="roster-section__head"> <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> <span class="roster-section__count"><%= members.size %></span>
</header> </header>
<div class="roster-list"> <div class="roster-list">
@@ -167,9 +167,9 @@
<% if @live_sessions.empty? && @upcoming_matches.empty? && @recordings.empty? %> <% if @live_sessions.empty? && @upcoming_matches.empty? && @recordings.empty? %>
<div class="empty-state empty-state--soft team-public-empty"> <div class="empty-state empty-state--soft team-public-empty">
<p><strong>Nessuna diretta o replay al momento.</strong></p> <p><strong><%= t("team_pages.show.empty_title") %></strong></p>
<p>Torna a controllare prima delle prossime gare: qui compariranno dirette, calendario e archivio della squadra.</p> <p><%= t("team_pages.show.empty_body") %></p>
<%= link_to "Vedi tutte le dirette", public_live_index_path, class: "btn btn-secondary" %> <%= link_to t("team_pages.show.empty_link"), public_live_index_path, class: "btn btn-secondary" %>
</div> </div>
<% end %> <% end %>
</div> </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" %> <% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px;max-width:520px"> <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> <h1><%= @member.full_name %></h1>
<%= render "shared/roster_member_form", member: @member, team: @team %> <%= render "shared/roster_member_form", member: @member, team: @team %>
</div> </div>

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