Sito marketing, SEO, legal, live programmata e branding Match Live Tv.

Logo e favicon nel header, partite programmate su web e mobile, reset password,
pagine FAQ/sitemap, privacy/termini GDPR e icona Android «Match Live Tv».

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 23:17:15 +02:00
parent 3a5649f482
commit 471291b2c4
70 changed files with 1601 additions and 153 deletions

View File

@@ -7,7 +7,7 @@ module Api
def index
matches = @team.matches
.includes(:team, :stream_sessions)
.order(scheduled_at: :desc)
.order(Arel.sql("scheduled_at ASC NULLS LAST"), created_at: :desc)
render json: matches.map { |m| match_json(m) }
end

View File

@@ -12,6 +12,15 @@ module Public
.search_by_team_or_opponent(@query)
.order(Arel.sql("started_at DESC NULLS LAST"), created_at: :desc)
busy_match_ids = StreamSession.where.not(status: %w[ended error]).select(:match_id)
@upcoming_matches = Match
.scheduled_upcoming
.includes(:team)
.search_teams_or_opponents(@query)
.where.not(id: busy_match_ids)
.order(scheduled_at: :asc)
.limit(30)
@online_paths = Mediamtx::Client.new.online_path_names
end

View File

@@ -15,5 +15,11 @@ module Public
def terms
end
def faq
end
def pallavolo
end
end
end

View File

@@ -0,0 +1,52 @@
module Public
class PasswordResetsController < WebBaseController
def new
end
def create
user = User.find_by(email: params[:email]&.downcase&.strip)
if user
token = user.generate_password_reset!
UserMailer.password_reset(user, token).deliver_now
end
redirect_to public_login_path,
notice: "Se l'email è registrata, riceverai a breve un link per reimpostare la password."
end
def edit
@user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path,
alert: "Link non valido o scaduto. Richiedi un nuovo reset password."
return
end
@token = params[:token]
end
def update
@user = User.find_by_password_reset_token(params[:token])
if @user.nil? || @user.password_reset_expired?
redirect_to new_public_password_reset_path,
alert: "Link non valido o scaduto. Richiedi un nuovo reset password."
return
end
if params[:password].blank? || params[:password].length < 8
flash.now[:alert] = "La password deve avere almeno 8 caratteri"
@token = params[:token]
return render :edit, status: :unprocessable_entity
end
if params[:password] != params[:password_confirmation]
flash.now[:alert] = "Le password non coincidono"
@token = params[:token]
return render :edit, status: :unprocessable_entity
end
@user.update!(password: params[:password])
@user.clear_password_reset!
redirect_to public_login_path, notice: "Password aggiornata. Ora puoi accedere."
end
end
end

View File

@@ -6,6 +6,12 @@ module Public
end
def create
unless params[:accept_terms] == "1"
@user = User.new(user_params.merge(role: "coach"))
flash.now[:alert] = "Devi accettare linformativa privacy e i termini di servizio per registrarti."
return render :new, status: :unprocessable_entity
end
@user = User.new(user_params.merge(role: "coach"))
if @user.save
session[:user_id] = @user.id

View File

@@ -2,6 +2,8 @@ module Public
class SiteBaseController < ActionController::Base
layout "marketing"
include ::SeoHelper
include ::LegalHelper
helper_method :current_user, :logged_in?
private

View File

@@ -0,0 +1,21 @@
module Public
class SitemapController < ActionController::Base
def show
base = MatchLiveTv.app_public_url.chomp("/")
@entries = [
{ loc: "#{base}/", changefreq: "weekly", priority: "1.0" },
{ loc: "#{base}/funzionalita", changefreq: "monthly", priority: "0.9" },
{ loc: "#{base}/prezzi", changefreq: "monthly", priority: "0.9" },
{ loc: "#{base}/pallavolo-giovanile", changefreq: "monthly", priority: "0.85" },
{ loc: "#{base}/faq", changefreq: "monthly", priority: "0.8" },
{ loc: "#{base}/live", changefreq: "hourly", priority: "0.85" },
{ loc: "#{base}/privacy", changefreq: "yearly", priority: "0.3" },
{ loc: "#{base}/termini", changefreq: "yearly", priority: "0.3" }
]
respond_to do |format|
format.xml { render layout: false }
end
end
end
end

View File

@@ -0,0 +1,5 @@
module LegalHelper
def legal_last_updated
"26 maggio 2026"
end
end

View File

@@ -15,6 +15,24 @@ module Public
partials.map { |p| format_set_partial_entry(p) }.join(" · ")
end
def live_scheduled_label(datetime)
return nil unless datetime
datetime.in_time_zone.strftime("%d/%m/%Y alle %H:%M")
end
def live_scheduled_relative(datetime)
return nil unless datetime
if datetime.to_date == Time.zone.today
"Oggi alle #{datetime.strftime('%H:%M')}"
elsif datetime.to_date == Time.zone.tomorrow
"Domani alle #{datetime.strftime('%H:%M')}"
else
live_scheduled_label(datetime)
end
end
def live_score_points_label(match, score_state)
return "" unless score_state

View File

@@ -0,0 +1,33 @@
module SeoHelper
DEFAULT_OG_IMAGE_PATH = "/og-share.png"
def seo_site_url
MatchLiveTv.app_public_url.chomp("/")
end
def seo_absolute_url(path)
path = "/#{path}" unless path.start_with?("/")
"#{seo_site_url}#{path}"
end
def seo_canonical_url
if content_for?(:canonical_url)
content_for(:canonical_url)
else
request.original_url.split("?").first
end
end
def seo_og_image_url
if content_for?(:og_image)
img = content_for(:og_image)
img.start_with?("http") ? img : seo_absolute_url(img)
else
seo_absolute_url(DEFAULT_OG_IMAGE_PATH)
end
end
def seo_robots_content
content_for?(:robots) ? content_for(:robots) : "index, follow"
end
end

View File

@@ -1,4 +1,4 @@
class ApplicationMailer < ActionMailer::Base
default from: "from@example.com"
default from: -> { MatchLiveTv.mail_from }
layout "mailer"
end

View File

@@ -0,0 +1,11 @@
class UserMailer < ApplicationMailer
default from: -> { MatchLiveTv.mail_from }
def password_reset(user, token)
@user = user
@reset_url = public_password_reset_url(token: token)
@expiry_hours = MatchLiveTv.password_reset_expiry_hours
mail to: user.email, subject: "Reimposta la password — Match Live TV"
end
end

View File

@@ -2,6 +2,27 @@ class Match < ApplicationRecord
belongs_to :team
has_many :stream_sessions, dependent: :destroy
ACTIVE_SESSION_STATUSES = %w[live connecting reconnecting paused].freeze
validates :opponent_name, presence: true
validates :sets_to_win, numericality: { greater_than: 0, less_than: 6 }
scope :scheduled_upcoming, -> {
where.not(scheduled_at: nil).where("scheduled_at >= ?", Time.current)
}
scope :search_teams_or_opponents, lambda { |query|
q = query.to_s.strip
return all if q.blank?
term = "%#{sanitize_sql_like(q)}%"
joins(:team).where(
"teams.name ILIKE :term OR matches.opponent_name ILIKE :term",
term: term
)
}
def broadcast_active?
stream_sessions.where(status: ACTIVE_SESSION_STATUSES).exists?
end
end

View File

@@ -14,4 +14,32 @@ class User < ApplicationRecord
def admin?
role == "admin"
end
def generate_password_reset!
raw = SecureRandom.urlsafe_base64(32)
update!(
password_reset_digest: User.digest_token(raw),
password_reset_sent_at: Time.current
)
raw
end
def password_reset_expired?
password_reset_sent_at.blank? ||
password_reset_sent_at < MatchLiveTv.password_reset_expiry_hours.hours.ago
end
def clear_password_reset!
update!(password_reset_digest: nil, password_reset_sent_at: nil)
end
def self.find_by_password_reset_token(token)
return if token.blank?
find_by(password_reset_digest: User.digest_token(token))
end
def self.digest_token(token)
Digest::SHA256.hexdigest(token.to_s)
end
end

View File

@@ -6,7 +6,7 @@
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
<%= render "shared/meta_tags" %>
<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=15">
<link rel="stylesheet" href="/marketing.css?v=20">
</head>
<body>
<%= render "shared/marketing_nav" %>

View File

@@ -6,8 +6,8 @@
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
<%= render "shared/meta_tags" %>
<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=15">
<link rel="stylesheet" href="/live.css">
<link rel="stylesheet" href="/marketing.css?v=20">
<link rel="stylesheet" href="/live.css?v=2">
<%= yield :head %>
</head>
<body>

View File

@@ -1,4 +1,6 @@
<% content_for :title, "Invito squadra" %>
<% content_for :title, "Invito squadra — Match Live TV" %>
<% content_for :meta_description, "Accetta l'invito per entrare nello staff della squadra su Match Live TV." %>
<% content_for :robots, "noindex, nofollow" %>
<div class="card" style="max-width:480px">
<h1>Accesso staff — <%= @invitation.team.name %></h1>

View File

@@ -1,76 +1,135 @@
<% content_for :title do %>Dirette in corso — Match Live TV<% end %>
<% content_for :title, "Dirette live partite giovanili in corso — Match Live TV" %>
<% content_for :meta_description, "Guarda le dirette live delle partite giovanili ora in corso e le prossime gare programmate. Cerca la tua squadra e apri il link senza installare app." %>
<% content_for :canonical_url, seo_absolute_url(public_live_index_path) %>
<div class="wrap">
<h1>Dirette in corso</h1>
<p class="results-hint">
Cerca per nome squadra o avversario, poi apri la diretta per guardare lo stream.
</p>
<%= form_with url: public_live_index_path, method: :get, local: true, class: "search-form" do %>
<input
type="search"
name="q"
value="<%= @query %>"
placeholder="Es. Tigers, Eagles, nome squadra…"
aria-label="Cerca squadra"
autocomplete="off"
/>
<button type="submit" class="btn btn-primary">Cerca</button>
<% if @query.present? %>
<%= link_to "Azzera", public_live_index_path, class: "btn btn-secondary" %>
<% end %>
<% end %>
<% if @query.present? %>
<h1>Dirette e partite in programma</h1>
<p class="results-hint">
Risultati per «<%= h @query %>»: <%= @sessions.size %> diretta<%= @sessions.size == 1 ? "" : "e" %>
Cerca la tua squadra: trovi le dirette attive e le partite già programmate dallapp.
</p>
<% end %>
<% if @sessions.any? %>
<div class="live-grid">
<% @sessions.each do |session| %>
<% match = session.match %>
<% on_air = @online_paths.include?(session.mediamtx_path_name) %>
<article class="live-card">
<h3><%= match.team.name %> vs <%= match.opponent_name %></h3>
<p class="meta">
<% if match.location.present? %><%= match.location %> · <% end %>
<%= session.platform == "matchlivetv" ? "Match Live TV" : session.platform.capitalize %>
</p>
<% if session.score_state %>
<p class="card-score">
<span class="card-sets"><%= live_score_sets_label(session.score_state) %></span>
<% if live_score_partials_label(session.score_state).present? %>
<span class="card-partials">Parziali: <%= live_score_partials_label(session.score_state) %></span>
<% end %>
<span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span>
</p>
<% end %>
<div class="badges">
<% if on_air %>
<span class="badge badge-on-air">In onda</span>
<% elsif session.status == "live" %>
<span class="badge badge-live">Live</span>
<% elsif session.status == "reconnecting" %>
<span class="badge badge-connecting">Riconnessione</span>
<% else %>
<span class="badge badge-wait">In avvio</span>
<% end %>
</div>
<%= link_to "Guarda diretta →", public_live_path(session), class: "btn-watch" %>
</article>
<% end %>
</div>
<% else %>
<div class="empty-state">
<%= form_with url: public_live_index_path, method: :get, local: true, class: "search-form" do %>
<input
type="search"
name="q"
value="<%= @query %>"
placeholder="Es. Tigers, Eagles, nome squadra…"
aria-label="Cerca squadra"
autocomplete="off"
/>
<button type="submit" class="btn btn-primary">Cerca</button>
<% if @query.present? %>
<p>Nessuna diretta attiva per «<%= h @query %>».</p>
<p>Prova un altro nome squadra o <%= link_to "mostra tutte le dirette", public_live_index_path %>.</p>
<% else %>
<p>Al momento non ci sono dirette attive.</p>
<p>Quando una squadra avvia lo streaming dallapp, la partita comparirà qui.</p>
<%= link_to "Azzera", public_live_index_path, class: "btn btn-secondary" %>
<% end %>
</div>
<% end %>
<% end %>
<% if @query.present? %>
<p class="results-hint">
Risultati per «<%= h @query %>»:
<%= @sessions.size %> in diretta · <%= @upcoming_matches.size %> in programma
</p>
<% end %>
<% if @sessions.any? %>
<h2 class="section-heading">In diretta adesso</h2>
<div class="live-grid">
<% @sessions.each do |session| %>
<% match = session.match %>
<% on_air = @online_paths.include?(session.mediamtx_path_name) %>
<article class="live-card">
<h3><%= match.team.name %> vs <%= match.opponent_name %></h3>
<p class="meta">
<% if match.location.present? %><%= match.location %> · <% end %>
<%= session.platform == "matchlivetv" ? "Match Live TV" : session.platform.capitalize %>
</p>
<% if session.score_state %>
<p class="card-score">
<span class="card-sets"><%= live_score_sets_label(session.score_state) %></span>
<% if live_score_partials_label(session.score_state).present? %>
<span class="card-partials">Parziali: <%= live_score_partials_label(session.score_state) %></span>
<% end %>
<span class="card-points"><%= session.score_state.home_points %> - <%= session.score_state.away_points %></span>
</p>
<% end %>
<div class="badges">
<% if on_air %>
<span class="badge badge-on-air">In onda</span>
<% elsif session.status == "live" %>
<span class="badge badge-live">Live</span>
<% elsif session.status == "reconnecting" %>
<span class="badge badge-connecting">Riconnessione</span>
<% else %>
<span class="badge badge-wait">In avvio</span>
<% end %>
</div>
<%= link_to "Guarda diretta →", public_live_path(session), class: "btn-watch" %>
</article>
<% end %>
</div>
<% end %>
<% if @upcoming_matches.any? %>
<h2 class="section-heading<%= ' section-heading--spaced' if @sessions.any? %>">Prossime partite</h2>
<p class="results-hint">Programmate dallapp: la diretta partirà quando lo staff avvierà la trasmissione.</p>
<div class="live-grid upcoming-grid">
<% @upcoming_matches.each do |match| %>
<article class="live-card live-card--upcoming">
<h3><%= match.team.name %> vs <%= match.opponent_name %></h3>
<p class="meta">
<% if match.location.present? %><%= match.location %> · <% end %>
<%= match.category.presence || match.sport&.capitalize %>
</p>
<p class="upcoming-when">
<%= live_scheduled_relative(match.scheduled_at) %>
</p>
<div class="badges">
<span class="badge badge-scheduled">In programma</span>
</div>
</article>
<% end %>
</div>
<% end %>
<% if @sessions.empty? %>
<% if @upcoming_matches.any? %>
<div class="empty-state empty-state--soft">
<p><strong>Nessuna diretta in questo momento.</strong></p>
<p>Torna allorario indicato: quando la squadra avvierà lo streaming, la partita comparirà in «In diretta adesso».</p>
</div>
<% else %>
<div class="empty-hero">
<div class="empty-hero-copy">
<h2>Nessuna diretta adesso — ma puoi già organizzarti</h2>
<p>
Se la tua squadra usa Match Live TV, dal telefono si programma la partita con data e ora.
Genitori e parenti trovano qui la diretta quando il fischio dinizio è vicino.
</p>
<ul class="empty-hero-list">
<li>Programma la gara dallapp (senza avviare subito la diretta)</li>
<li>La partita compare in «Prossime partite» con orario e campo</li>
<li>Lo streaming parte solo quando premi avvio in palestra</li>
</ul>
<p class="empty-hero-cta">
<%= link_to "Registra la tua squadra", public_signup_path, class: "btn btn-primary" %>
<%= link_to "Scopri come funziona", public_features_path, class: "btn btn-secondary" %>
</p>
</div>
<aside class="demo-live-card" aria-label="Esempio di diretta attiva">
<p class="demo-label">Esempio — così appare una diretta attiva</p>
<article class="live-card live-card--demo">
<h3>Tigers Volley vs ASD Eagles</h3>
<p class="meta">PalaTigers · Match Live TV</p>
<p class="card-score">
<span class="card-sets">Set 2 · Set vinti 1-0</span>
<span class="card-points">18 - 16</span>
</p>
<div class="badges">
<span class="badge badge-on-air">In onda</span>
</div>
<span class="btn-watch btn-watch--demo">Guarda diretta →</span>
</article>
</aside>
</div>
<% end %>
<% end %>
</div>

View File

@@ -1,4 +1,6 @@
<% content_for :title do %><%= @match.team.name %> vs <%= @match.opponent_name %> — Match Live TV<% end %>
<% content_for :title do %><%= @match.team.name %> vs <%= @match.opponent_name %> — Diretta live<% end %>
<% content_for :meta_description do %>Segui in diretta live <%= @match.team.name %> vs <%= @match.opponent_name %><% if @match.location.present? %> — <%= @match.location %><% end %>. Guarda dal browser senza installare app.<% end %>
<% content_for :robots, "noindex, follow" %>
<% content_for :head do %>
<% unless @stream_closed %>

View File

@@ -0,0 +1,83 @@
<% content_for :title, "FAQ — Diretta live partite giovanili | Match Live TV" %>
<% content_for :meta_description, "Domande frequenti su streaming partite giovanili: come guardare la diretta senza app, costi, archivio, pallavolo Under 16, inviti staff e piani Free e Premium." %>
<% content_for :canonical_url, seo_absolute_url(public_faq_path) %>
<div class="wrap seo-page">
<h1 style="margin-top:24px">Domande frequenti sullo streaming delle partite giovanili</h1>
<p class="seo-lead">
Risposte per genitori, allenatori e dirigenti che vogliono mandare in diretta live le partite
di pallavolo, calcio e altri sport dilettantistici.
</p>
<div class="faq-list">
<details class="faq-item" open>
<summary>Come guardo la partita di mio figlio senza installare app?</summary>
<p>
La società condivide un link (via WhatsApp, email o gruppo squadra). Apri il link dal telefono o dal computer:
vedi la <strong>diretta live</strong> nel browser, senza registrarti. Funziona per nonni, parenti e amici lontani.
</p>
</details>
<details class="faq-item">
<summary>Chi può avviare la diretta dalla palestra?</summary>
<p>
Coach, dirigenti o volontari invitati dalla società. Usano lapp Match Live TV sul telefono per filmare e,
se serve, un secondo telefono per aggiornare il punteggio. La diretta parte solo quando premo avvio in palestra.
</p>
</details>
<details class="faq-item">
<summary>È adatto alla pallavolo giovanile (Under 14, Under 16)?</summary>
<p>
Sì. Il punteggio a set è pensato per la pallavolo; puoi personalizzare regole per tornei particolari.
Scopri la pagina dedicata: <%= link_to "Match Live TV per pallavolo giovanile", public_pallavolo_path %>.
</p>
</details>
<details class="faq-item">
<summary>Cosa succede se perdo la diretta?</summary>
<p>
Con i piani Premium Light o Full la partita viene salvata in archivio (30 o 90 giorni) e puoi rivederla dal sito.
Il piano Free non include replay su server, ma la diretta resta gratuita per chi trasmette e per chi guarda.
</p>
</details>
<details class="faq-item">
<summary>Quanto costa per la società?</summary>
<p>
Puoi iniziare con il piano <strong>Free</strong> (limiti su staff e una diretta alla volta).
Premium Light e Full aggiungono più partite in parallelo, archivio e YouTube.
<%= link_to "Confronta i prezzi", public_prezzi_path %>.
</p>
</details>
<details class="faq-item">
<summary>Posso programmare una partita prima del giorno gara?</summary>
<p>
Sì, dallapp si imposta data e ora: la partita compare in «Prossime partite» sul sito.
Lo streaming parte solo quando lo staff avvia la trasmissione in palestra.
</p>
</details>
<details class="faq-item">
<summary>La diretta va anche su YouTube?</summary>
<p>
Con Premium Light puoi mandarla sul canale Match TV Light; con Premium Full anche sul canale YouTube della società.
Chi preferisce resta sul link Match Live TV, comodo per le famiglie.
</p>
</details>
<details class="faq-item">
<summary>Devo aprire porte sul router o avere attrezzatura da TV?</summary>
<p>
No per chi guarda. Per la società che trasmette basta lo smartphone e una buona connessione in palestra;
lo staff tecnico della piattaforma gestisce linfrastruttura. Niente camere broadcast o mixer.
</p>
</details>
</div>
<p style="text-align:center;margin:40px 0">
<%= link_to "Registra la squadra", public_signup_path, class: "btn btn-primary" %>
<%= link_to "Guarda le dirette", public_live_index_path, class: "btn btn-secondary" %>
</p>
</div>

View File

@@ -1,24 +1,25 @@
<% content_for :title, "Funzionalità — Match Live TV" %>
<% content_for :meta_description, "Camera, regia, punteggio live, gestione staff, archivio gare e YouTube con Premium Light e Full." %>
<% content_for :title, "Funzionalità streaming partite giovanili — Match Live TV" %>
<% content_for :meta_description, "App per diretta live da telefono: filma la partita, aggiorna il punteggio, condividi il link con genitori e nonni. Archivio gare e piani per società sportive giovanili." %>
<% content_for :canonical_url, seo_absolute_url(public_features_path) %>
<div class="wrap">
<h1 style="margin-top:24px">Funzionalità</h1>
<p style="color:#aaa;max-width:640px">Tutto ciò che serve per trasmettere partite giovanili in modo professionale, senza complessità da broadcaster TV.</p>
<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>
<section class="section">
<h2>Per la società e lo staff</h2>
<div class="features-grid">
<div class="feature-card">
<h3>Staff con email</h3>
<p>Delega trasmissione e regia a coach e volontari: ognuno ha credenziali personali.</p>
<h3>Inviti con la propria email</h3>
<p>Il presidente o lowner invita chi filma e chi aggiorna il punteggio: ognuno accede con la sua email, senza condividere password.</p>
</div>
<div class="feature-card">
<h3>Revoca accessi</h3>
<p>L'owner rimuove membri dello staff e annulla richieste pendenti dalla dashboard web.</p>
<h3>Gestione accessi</h3>
<p>Puoi togliere laccesso a un volontario o annullare un invito ancora in sospeso dalla dashboard della squadra.</p>
</div>
<div class="feature-card">
<h3>Limiti per piano</h3>
<p>Posti staff (trasmissione e regia) e partite concorrenti scalano con Free, Premium Light e Premium Full.</p>
<h3>Piani su misura</h3>
<p>Con Free, Premium Light o Premium Full aumenti quante persone possono aiutare in diretta e quante partite possono andare in parallelo.</p>
</div>
</div>
</section>
@@ -27,32 +28,32 @@
<h2>In campo e in diretta</h2>
<div class="features-grid">
<div class="feature-card">
<h3>Modalità camera</h3>
<p>Streaming RTMP resiliente: la diretta continua anche con reti instabili.</p>
<h3>Filma dal telefono</h3>
<p>Appoggi il telefono e avvii la diretta: anche con il 4G debole la trasmissione resta stabile il più possibile, senza impazzire con cavi o mixer.</p>
</div>
<div class="feature-card">
<h3>Regia separata</h3>
<p>Secondo dispositivo per aggiornare il punteggio via QR pairing.</p>
<h3>Punteggio da un secondo telefono</h3>
<p>Un altro volontario aggiorna set e punti da un secondo cellulare: basta inquadrare il codice che compare sullo schermo di chi filma.</p>
</div>
<div class="feature-card">
<h3>Pagina pubblica /live</h3>
<p>Genitori e tifosi guardano dal browser senza installare nulla.</p>
<h3>Link per famiglie e tifosi</h3>
<p>Condividi un link: genitori, nonni e amici guardano dal browser del telefono o del computer, senza installare app.</p>
</div>
<div class="feature-card">
<h3>Archivio replay</h3>
<p>Premium Light/Full: salvataggio server (30 o 90 giorni) e download su telefono.</p>
<h3>Archivio partite</h3>
<p>Con Premium Light o Full le gare restano salvate sul server (30 o 90 giorni) e puoi scaricarle sul telefono quando serve.</p>
</div>
<div class="feature-card">
<h3>YouTube Match TV Light</h3>
<p>Premium Light: le dirette sono visibili anche sul canale YouTube Match TV Light.</p>
<h3>Anche su YouTube (Match TV Light)</h3>
<p>Con Premium Light la diretta è visibile anche sul canale YouTube Match TV Light, oltre al link Match Live TV.</p>
</div>
<div class="feature-card">
<h3>YouTube della società</h3>
<p>Premium Full: relay verso il canale YouTube del club.</p>
<h3>Canale YouTube della società</h3>
<p>Con Premium Full puoi mandare la stessa diretta anche sul canale YouTube ufficiale del club.</p>
</div>
<div class="feature-card">
<h3>Sito della società</h3>
<p>Integrazione embed sul sito del club — in arrivo con Premium Full.</p>
<h3>Sul sito della società</h3>
<p>Integrazione per incorporare il player sul sito del club — in arrivo con Premium Full.</p>
</div>
</div>
</section>

View File

@@ -1,5 +1,6 @@
<% content_for :title, "Match Live TV — Ogni partita, ogni evento, per chi non può esserci" %>
<% content_for :meta_description, "Trasmetti le partite dallo smartphone. Nonni, parenti e amici guardano dal browser, senza app. Se te la perdi, la ritrovi nell'archivio." %>
<% content_for :title, "Match Live TV — Diretta live partite giovanili da telefono" %>
<% content_for :meta_description, "Non puoi andare in palestra? Guarda la diretta live della partita di tuo figlio dal telefono o dal computer, senza installare app. Pallavolo, calcio e sport giovanili: link da condividere con nonni e parenti, archivio se te la perdi." %>
<% content_for :canonical_url, seo_absolute_url(root_path) %>
<section class="hero hero-split">
<div class="wrap hero-grid">
@@ -41,7 +42,7 @@
</section>
<section class="section wrap">
<h2>Come funziona</h2>
<h2>Come funziona lo streaming per squadre giovanili</h2>
<div class="steps">
<div class="step step--visual">
<div class="step-visual">
@@ -79,10 +80,30 @@
</section>
<section class="section wrap plans-teaser">
<h2>Piani per ogni esigenza</h2>
<h2>Piani per dirette live e archivio partite</h2>
<p class="plans-teaser-lead">Inizia gratis. Passa a Premium quando vuoi più partite in contemporanea, archivio più lungo e diretta anche su YouTube.</p>
<div class="plans-teaser-visual">
<%= image_tag "/home-piani-ecosistema.png?v=1", alt: "Ecosistema Match Live TV: smartphone in diretta, laptop con live e statistiche, tablet con archivio partite e cloud", class: "plans-teaser-img", loading: "lazy" %>
</div>
<%= link_to "Confronta i piani", public_prezzi_path, class: "btn btn-primary" %>
</section>
<section class="section wrap seo-prose">
<h2>Streaming partite giovanili: perché le famiglie scelgono Match Live TV</h2>
<p>
Allenatori e dirigenti cercano un modo semplice per mandare in <strong>diretta live</strong> le partite
di <strong>pallavolo giovanile</strong>, calcio, basket e altri sport: senza attrezzature da TV,
solo con lo smartphone in palestra. Match Live TV è pensato per le società dilettantistiche che vogliono
far guardare <strong>Under 14, Under 16, Under 18</strong> e settori giovanili a chi è lontano.
</p>
<p>
Un genitore che lavora, un nonno che non può viaggiare, un parente allestero: aprono un link e seguono
la partita dal browser. Se arrivano in ritardo, la <strong>registrazione in archivio</strong> (con i piani Premium)
resta disponibile per giorni. Niente account complicati per chi guarda — solo per lo staff che trasmette.
</p>
<p>
Hai dubbi su come funziona? Leggi le <%= link_to "domande frequenti", public_faq_path %>,
scopri <%= link_to "Match Live TV per la pallavolo giovanile", public_pallavolo_path %>
o <%= link_to "registra la squadra gratis", public_signup_path %>.
</p>
</section>

View File

@@ -0,0 +1,67 @@
<% content_for :title, "Diretta live pallavolo giovanile — Under 14, Under 16 | Match Live TV" %>
<% content_for :meta_description, "Streaming partite di pallavolo giovanile da telefono: diretta live per genitori e nonni, punteggio a set, link da condividere. Ideale per società Under 14, Under 16 e Under 18." %>
<% content_for :canonical_url, seo_absolute_url(public_pallavolo_path) %>
<div class="wrap seo-page">
<h1 style="margin-top:24px">Match Live TV per la pallavolo giovanile</h1>
<p class="seo-lead">
La soluzione per società, coach e genitori che vogliono una <strong>diretta live</strong> affidabile
delle partite <strong>Under 14, Under 16, Under 18</strong> e settori giovanili — senza diventare telecronisti.
</p>
<section class="seo-prose">
<h2>Perché la pallavolo giovanile ha bisogno di uno streaming semplice</h2>
<p>
In palestra i genitori non sono sempre tutti in tribuna: turni di lavoro, fratelli piccoli, trasferte.
Un nonno in unaltra città o un parente allestero vuole comunque vedere il tie-break del terzo set.
Match Live TV nasce per questo: <strong>streaming partite pallavolo giovanile</strong> dal telefono,
con punteggio aggiornato e link immediato da mandare su WhatsApp.
</p>
<h2>Cosa fa il coach in due minuti</h2>
<ul>
<li>Programma la partita con data, avversario e campo dallapp</li>
<li>Il giorno gara appoggia il telefono e avvia la diretta</li>
<li>Un volontario può aggiornare set e punti da un secondo cellulare</li>
<li>Condivide il link con il gruppo genitori: tutti guardano dal browser</li>
</ul>
<h2>Cosa vede la famiglia a casa</h2>
<p>
Nessuna app da installare. Apri il link e segui la partita in diretta live: nome squadre, punteggio,
stato del set. Se hai perso linizio, con Premium la gara resta in archivio per rivederla quando vuoi.
</p>
<h2>Pallavolo: punteggio e regole</h2>
<p>
Set da vincere 2 o 3, punti 25/21/15 configurabili per tornei non standard. La regia è separata dalla ripresa:
in campo resta concentrato sul gioco, non sul telefono.
</p>
<h2>Altri sport giovanili</h2>
<p>
La stessa piattaforma funziona anche per calcio, basket e sport di squadra dilettantistici.
La pallavolo resta lo sport con cui molte società iniziano perché il punteggio a set è già integrato.
</p>
</section>
<div class="features-grid" style="margin-top:32px">
<div class="feature-card">
<h3>Genitori e nonni</h3>
<p>Link su WhatsApp, visione da telefono o TV con browser. Zero registrazione per chi guarda.</p>
</div>
<div class="feature-card">
<h3>Società e staff</h3>
<p>Inviti con email, più operatori con Premium, revoca accessi dalla dashboard web.</p>
</div>
<div class="feature-card">
<h3>Archivio gare</h3>
<p>Premium Light/Full: replay per chi ha perso la diretta o vuole rivedere un punto chiave.</p>
</div>
</div>
<p style="text-align:center;margin:40px 0">
<%= link_to "Inizia gratis", public_signup_path, class: "btn btn-primary" %>
<%= link_to "Domande frequenti", public_faq_path, class: "btn btn-secondary" %>
</p>
</div>

View File

@@ -1,4 +1,6 @@
<% content_for :title, "Prezzi — Match Live TV" %>
<% content_for :title, "Prezzi streaming partite giovanili — Match Live TV" %>
<% content_for :meta_description, "Piani Free, Premium Light e Premium Full per dirette live e archivio partite. Abbonamento annuale per società: più staff, più partite in parallelo, replay e YouTube." %>
<% content_for :canonical_url, seo_absolute_url(public_prezzi_path) %>
<div class="wrap" style="padding-top:24px">
<h1>Piani per la tua squadra</h1>

View File

@@ -1,9 +1,188 @@
<% content_for :title, "Privacy — Match Live TV" %>
<div class="wrap" style="padding-top:24px">
<h1>Informativa privacy</h1>
<div class="card">
<p>Match Live TV tratta i dati necessari per account, gestione staff e streaming (email, nome, dati di utilizzo delle sessioni).</p>
<p>Per richieste o cancellazione account contattare il gestore del servizio all'indirizzo indicato nella dashboard della squadra.</p>
<p style="color:#888;font-size:0.9rem">Documento provvisorio — da completare con i dati del titolare del trattamento.</p>
</div>
<% content_for :title, "Informativa privacy — Match Live TV" %>
<% content_for :meta_description, "Informativa privacy GDPR di Match Live TV: dati account, streaming partite giovanili, immagini di minori, pagamenti Stripe e diritti degli interessati." %>
<% content_for :canonical_url, seo_absolute_url(public_privacy_path) %>
<div class="wrap legal-doc">
<h1>Informativa sul trattamento dei dati personali</h1>
<p class="legal-meta">Ultimo aggiornamento: <%= legal_last_updated %> · Ai sensi del Regolamento (UE) 2016/679 (GDPR)</p>
<section>
<h2>1. Titolare del trattamento</h2>
<p>
Il titolare del trattamento dei dati personali raccolti tramite il sito e i servizi Match Live TV è:
</p>
<ul>
<li><strong><%= MatchLiveTv.privacy_controller_name %></strong></li>
<li>Indirizzo: <%= MatchLiveTv.privacy_controller_address %></li>
<li>Email per privacy e diritti: <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a></li>
<% if MatchLiveTv.privacy_controller_vat.present? %>
<li>Partita IVA / CF: <%= MatchLiveTv.privacy_controller_vat %></li>
<% end %>
</ul>
<p>Per richieste relative a questa informativa o per esercitare i diritti previsti dal GDPR, scrivi allindirizzo email indicato.</p>
</section>
<section>
<h2>2. Ambito di applicazione</h2>
<p>La presente informativa si applica a:</p>
<ul>
<li>visitatori del sito web Match Live TV (pagine informative, elenco dirette, registrazione squadra);</li>
<li>utenti registrati (referenti di società sportive, coach, staff invitato);</li>
<li>spettatori che accedono alle dirette tramite link (in genere senza creare un account);</li>
<li>utilizzo dellapp mobile collegata al servizio.</li>
</ul>
</section>
<section>
<h2>3. Tipologie di dati trattati</h2>
<p>Possiamo trattare, a seconda delluso del servizio:</p>
<ul>
<li><strong>Dati identificativi e di contatto:</strong> nome, indirizzo email, credenziali di accesso (password conservata in forma crittografata).</li>
<li><strong>Dati della squadra:</strong> nome società, sport, partite programmate, avversario, luogo, orari.</li>
<li><strong>Dati di streaming e tecnici:</strong> stato sessione, punteggio, telemetria di rete/bitrate, log di connessione al servizio video.</li>
<li><strong>Contenuti audiovisivi:</strong> flussi video e audio delle partite trasmesse in diretta e, con i piani Premium, registrazioni (replay) conservate per un periodo limitato.</li>
<li><strong>Dati di pagamento:</strong> per i piani a pagamento, Stripe tratta i dati necessari al pagamento (es. metodo di pagamento, intestazione fattura). Il titolare non conserva i numeri completi della carta.</li>
<li><strong>Dati di navigazione:</strong> log del server, indirizzo IP, user agent, necessari per sicurezza e funzionamento del sito.</li>
</ul>
</section>
<section>
<h2>4. Trattamento di immagini e dati di atleti minorenni</h2>
<p>
Match Live TV è destinato anche a competizioni e categorie giovanili (es. Under 14, Under 16, Under 18).
Le dirette possono riprendere <strong>atleti minorenni</strong> identificabili (immagine, voce, numero di maglia, dati di gioco).
</p>
<p><strong>Chi è responsabile del trattamento delle immagini dei minori</strong></p>
<p>
La <strong>società sportiva</strong> che avvia la trasmissione, in qualità di organizzatrice dellevento sportivo,
è in primo luogo responsabile di assicurare una base giuridica valida (tipicamente <strong>consenso dei genitori/tutori</strong>,
oppure altro titolo previsto dalla legge e dal regolamento della federazione di riferimento) prima di filmare e diffondere le immagini.
</p>
<p><strong>Ruolo di Match Live TV</strong></p>
<p>
Il titolare del servizio fornisce la piattaforma tecnica e, per le trasmissioni effettuate dagli utenti autorizzati,
può agire come <strong>responsabile del trattamento</strong> su istruzioni della società o, in alcuni casi, come titolare
per i trattamenti strettamente necessari allerogazione del servizio (hosting, sicurezza, fatturazione).
</p>
<p><strong>Misure richieste alla società sportiva</strong></p>
<ul>
<li>informare genitori/tutori sulluso di video in diretta e in archivio;</li>
<li>raccogliere e documentare i consensi necessari prima della diretta;</li>
<li>limitare la diffusione del link a persone autorizzate (familiari, staff);</li>
<li>non pubblicare su canali aperti se non previsto dal consenso o dal regolamento sportivo;</li>
<li>richiedere rimozione di registrazioni quando ne ha diritto linteressato o il tutore.</li>
</ul>
<p>
Le richieste dei genitori/tutori relative a immagini di minori possono essere inoltrate alla società sportiva
e, per supporto tecnico o cancellazione da archivio, anche a
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.
</p>
</section>
<section>
<h2>5. Finalità e basi giuridiche</h2>
<table class="legal-table">
<thead>
<tr><th>Finalità</th><th>Base giuridica (art. 6 GDPR)</th></tr>
</thead>
<tbody>
<tr>
<td>Registrazione account, gestione squadra e staff</td>
<td>Esecuzione del contratto / misure precontrattuali</td>
</tr>
<tr>
<td>Erogazione dirette, punteggio live, archivio (piani Premium)</td>
<td>Esecuzione del contratto; legittimo interesse alla sicurezza del servizio</td>
</tr>
<tr>
<td>Pagamenti e fatturazione (Stripe)</td>
<td>Esecuzione del contratto; obblighi di legge</td>
</tr>
<tr>
<td>Assistenza, sicurezza, prevenzione abusi</td>
<td>Legittimo interesse; obblighi di legge</td>
</tr>
<tr>
<td>Comunicazioni di servizio (es. reset password)</td>
<td>Esecuzione del contratto</td>
</tr>
<tr>
<td>Marketing diretto (solo se previsto e con consenso ove richiesto)</td>
<td>Consenso o legittimo interesse, secondo i casi</td>
</tr>
</tbody>
</table>
</section>
<section>
<h2>6. Destinatari e responsabili del trattamento</h2>
<p>I dati possono essere comunicati a fornitori che agiscono come responsabili del trattamento, tra cui:</p>
<ul>
<li>fornitore di hosting e infrastruttura cloud/server;</li>
<li><strong>Stripe</strong> (pagamenti) — <a href="https://stripe.com/it/privacy" rel="noopener" target="_blank">privacy Stripe</a>;</li>
<li>fornitore streaming video (MediaMTX / infrastruttura RTMP-HLS);</li>
<li>eventuale integrazione <strong>YouTube</strong> se attivata dalla società (soggetto a policy Google);</li>
<li>fornitore email transazionale (invio link reset password, inviti).</li>
</ul>
<p>I dati non sono venduti a terzi per finalità di profilazione commerciale.</p>
</section>
<section>
<h2>7. Trasferimenti extra SEE</h2>
<p>
Alcuni fornitori (es. Stripe, Google/YouTube) possono trattare dati anche fuori dallo Spazio Economico Europeo.
In tal caso si applicano garanzie adeguate previste dal GDPR (clausole contrattuali standard, decisioni di adeguatezza).
</p>
</section>
<section>
<h2>8. Conservazione</h2>
<ul>
<li><strong>Account utente:</strong> per tutta la durata del rapporto contrattuale e, dopo la cessazione, per il tempo necessario a obblighi di legge o contestazioni (in genere fino a 10 anni per documenti contabili ove applicabile).</li>
<li><strong>Registrazioni partite (replay):</strong> secondo il piano (es. 30 o 90 giorni), poi cancellazione automatica o su richiesta.</li>
<li><strong>Log tecnici:</strong> periodo limitato (es. 3090 giorni) salvo obblighi di sicurezza.</li>
<li><strong>Token reset password:</strong> validità massima <%= MatchLiveTv.password_reset_expiry_hours %> ore.</li>
</ul>
</section>
<section>
<h2>9. Diritti degli interessati</h2>
<p>In qualità di interessato hai diritto di:</p>
<ul>
<li>accedere ai tuoi dati e ottenerne copia;</li>
<li>rettificarli se inesatti;</li>
<li>chiederne la cancellazione (nei limiti di legge);</li>
<li>limitare o opporti al trattamento, ove applicabile;</li>
<li>revocare il consenso senza pregiudicare la liceità del trattamento basato sul consenso prestato prima della revoca;</li>
<li>proporre reclamo allAutorità Garante per la protezione dei dati personali (<a href="https://www.garanteprivacy.it" rel="noopener" target="_blank">garanteprivacy.it</a>).</li>
</ul>
<p>Per esercitare i diritti: <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.</p>
</section>
<section>
<h2>10. Sicurezza</h2>
<p>
Adottiamo misure tecniche e organizzative adeguate (accesso autenticato, password crittografate,
comunicazioni HTTPS, segregazione ambienti, limitazione accessi staff). Nessun sistema è invulnerabile:
se sospetti un accesso non autorizzato, cambia password e contattaci.
</p>
</section>
<section>
<h2>11. Cookie e tecnologie simili</h2>
<p>
Il sito utilizza cookie tecnici necessari al funzionamento (es. sessione di login) e, in futuro,
eventuali strumenti di analisi solo previo consenso ove richiesto. Per dettagli aggiornati contatta il titolare.
</p>
</section>
<section>
<h2>12. Modifiche</h2>
<p>
Il titolare può aggiornare questa informativa. La data in alto indica lultima revisione.
In caso di modifiche rilevanti, sarà fornita comunicazione adeguata (es. avviso sul sito o email agli account registrati).
</p>
</section>
<p class="legal-back"><%= link_to "← Torna alla home", root_path %></p>
</div>

View File

@@ -1,9 +1,134 @@
<% content_for :title, "Termini di servizio — Match Live TV" %>
<div class="wrap" style="padding-top:24px">
<h1>Termini di servizio</h1>
<div class="card">
<p>L'uso di Match Live TV implica l'accettazione dei limiti del piano sottoscritto (staff, dirette concorrenti, archivio).</p>
<p>Le dirette sono responsabilità della squadra titolare dell'account. È vietato l'uso per contenuti non conformi alle norme sulla protezione dei minori.</p>
<p style="color:#888;font-size:0.9rem">Documento provvisorio — da rivedere con consulenza legale.</p>
</div>
<% 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 :canonical_url, seo_absolute_url(public_termini_path) %>
<div class="wrap legal-doc">
<h1>Termini e condizioni di servizio</h1>
<p class="legal-meta">Ultimo aggiornamento: <%= legal_last_updated %></p>
<section>
<h2>1. Oggetto</h2>
<p>
I presenti Termini regolano luso della piattaforma <strong>Match Live TV</strong> (sito web, applicazione mobile
e servizi connessi) per la gestione di dirette sportive dilettantistiche/giovanili, punteggio live,
condivisione link agli spettatori e funzioni Premium (archivio, YouTube, ecc.).
</p>
<p>
Il servizio è erogato da <strong><%= MatchLiveTv.privacy_controller_name %></strong>
(di seguito «Fornitore»). Registrandoti o utilizzando il servizio accetti integralmente questi Termini
e l<%= link_to "informativa privacy", public_privacy_path %>.
</p>
</section>
<section>
<h2>2. Chi può usare il servizio</h2>
<ul>
<li>Il referente che registra la squadra dichiara di avere almeno <strong>18 anni</strong> e poteri per vincolare la società sportiva.</li>
<li>Lo staff invitato (trasmissione, regia) usa credenziali personali e si impegna a custodirle.</li>
<li>Gli spettatori che aprono il link della diretta di solito non necessitano di account.</li>
</ul>
</section>
<section>
<h2>3. Account, piani e pagamenti</h2>
<p>
Sono disponibili piani Free e Premium (Light / Full) con limiti su staff, dirette concorrenti, archivio e integrazioni.
I prezzi sono indicati nella pagina <%= link_to "Prezzi", public_prezzi_path %>.
</p>
<p>
I pagamenti dei piani a pagamento sono gestiti da <strong>Stripe</strong>. Accettando il checkout,
accetti anche i termini di Stripe per il pagamento. Il Fornitore non memorizza i dati completi della carta.
</p>
<p>Rinnovi, disdetta e diritto di recesso seguono quanto comunicato al momento dellacquisto e la normativa applicabile.</p>
</section>
<section>
<h2>4. Dirette, contenuti e protezione dei minori</h2>
<p><strong>Responsabilità della società sportiva</strong></p>
<p>
La società che avvia una diretta è responsabile dei contenuti trasmessi (immagini, audio, commenti)
e della conformità alle leggi, al regolamento federale e ai consensi necessari, in particolare quando
sono ripresi <strong>atleti minorenni</strong>.
</p>
<p>La società si impegna a:</p>
<ul>
<li>ottenere e conservare i consensi genitoriali/tutori o altra base giuridica prima della ripresa;</li>
<li>informare genitori e atleti su dove e per quanto tempo è visibile la diretta/archivio;</li>
<li>non trasmettere contenuti illegali, diffamatori, violenti o non pertinenti allevento sportivo;</li>
<li>rispettare il diritto allimmagine e alla voce di tutte le persone inquadrate.</li>
</ul>
<p>
Il Fornitore può sospendere o interrompere trasmissioni e account in caso di segnalazioni fondate
o violazioni gravi, senza pregiudizio di azioni legali.
</p>
</section>
<section>
<h2>5. Uso consentito e divieti</h2>
<p>È vietato:</p>
<ul>
<li>usare il servizio per finalità non sportive o commerciali non autorizzate;</li>
<li>aggirare limiti tecnici o di piano (staff, dirette concorrenti);</li>
<li>condividere credenziali o pubblicare link su canali che espongono minori a rischi non previsti dal consenso;</li>
<li>caricare malware, effettuare attacchi o interferire con linfrastruttura;</li>
<li>rivendere il servizio senza accordo scritto con il Fornitore.</li>
</ul>
</section>
<section>
<h2>6. Proprietà intellettuale</h2>
<p>
Marchi, software, design e documentazione del servizio restano del Fornitore o dei suoi licenzianti.
La società sportiva conserva i diritti sui propri contenuti video; concede al Fornitore una licenza
limitata a ospitarli, elaborarli e distribuirli tramite la piattaforma per erogare il servizio.
</p>
</section>
<section>
<h2>7. Disponibilità e limitazioni</h2>
<p>
Il servizio dipende da reti mobili, hardware e fornitori terzi. Non garantiamo assenza assoluta di interruzioni,
ritardi o perdita di dati, pur adottando misure ragionevoli. La qualità video può variare in base alla connessione in palestra.
</p>
</section>
<section>
<h2>8. Limitazione di responsabilità</h2>
<p>
Nei limiti consentiti dalla legge italiana, il Fornitore non risponde di danni indiretti, perdita di profitto,
o contenuti trasmessi dagli utenti. La responsabilità complessiva verso un cliente professionale non eccede,
ove possibile, limporto pagato negli ultimi 12 mesi per il piano sottoscritto.
</p>
<p>Nulla esclude responsabilità per dolo o colpa grave, né diritti inderogabili del consumatore ove applicabili.</p>
</section>
<section>
<h2>9. Recesso, sospensione e cancellazione</h2>
<p>
Puoi richiedere la chiusura dellaccount scrivendo a
<a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>.
Il Fornitore può sospendere laccount per inadempienza, mancato pagamento o violazione dei Termini.
Alla cessazione, i dati sono trattati come indicato in privacy (conservazione limitata o cancellazione).
</p>
</section>
<section>
<h2>10. Legge applicabile e foro</h2>
<p>
I Termini sono regolati dalla <strong>legge italiana</strong>. Per controversie con consumatori si applicano
le norme inderogabili di protezione del consumatore e il foro del consumatore ove previsto.
Per clienti professionali, foro competente in Italia presso la sede del Fornitore, salvo diverso accordo.
</p>
</section>
<section>
<h2>11. Contatti</h2>
<p>
<%= MatchLiveTv.privacy_controller_name %><br>
<%= MatchLiveTv.privacy_controller_address %><br>
Email: <a href="mailto:<%= MatchLiveTv.privacy_controller_email %>"><%= MatchLiveTv.privacy_controller_email %></a>
</p>
</section>
<p class="legal-back"><%= link_to "← Torna alla home", root_path %> · <%= link_to "Privacy", public_privacy_path %></p>
</div>

View File

@@ -0,0 +1,17 @@
<% content_for :title, "Nuova password — Match Live TV" %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>Scegli una nuova password</h1>
<div class="card">
<%= form_with url: public_password_reset_path, method: :patch, local: true do %>
<%= hidden_field_tag :token, @token %>
<%= label_tag :password, "Nuova password (min. 8 caratteri)" %>
<%= password_field_tag :password, required: true, minlength: 8, autocomplete: "new-password" %>
<%= label_tag :password_confirmation, "Conferma password" %>
<%= password_field_tag :password_confirmation, required: true, autocomplete: "new-password" %>
<%= submit_tag "Salva password", class: "btn btn-primary" %>
<% end %>
<p class="auth-footer"><%= link_to "Torna al login", public_login_path %></p>
</div>
</section>

View File

@@ -0,0 +1,15 @@
<% content_for :title, "Password dimenticata — Match Live TV" %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>Password dimenticata</h1>
<p class="auth-lead">Inserisci lemail dellaccount: ti invieremo un link per reimpostare la password.</p>
<div class="card">
<%= form_with url: public_password_forgot_path, method: :post, local: true do %>
<%= label_tag :email, "Email" %>
<%= email_field_tag :email, params[:email], required: true, autocomplete: "email" %>
<%= submit_tag "Invia link di reset", class: "btn btn-primary" %>
<% end %>
<p class="auth-footer"><%= link_to "Torna al login", public_login_path %></p>
</div>
</section>

View File

@@ -1,4 +1,6 @@
<% content_for :title, "Registrati — Match Live TV" %>
<% content_for :title, "Registra la squadra — Match Live TV" %>
<% content_for :meta_description, "Iscrivi la tua società sportiva giovanile su Match Live TV: dirette live da telefono, link per genitori e piani Free o Premium." %>
<% content_for :robots, "noindex, follow" %>
<section class="auth-page">
<h1>Registra la squadra</h1>
@@ -13,6 +15,18 @@
<%= f.password_field :password, required: true, minlength: 8 %>
<%= f.label :password_confirmation, "Conferma password" %>
<%= f.password_field :password_confirmation, required: true %>
<label class="legal-accept">
<%= check_box_tag :accept_terms, "1", false, required: true %>
<span class="legal-accept-text">
Ho letto e accetto l<%= link_to "informativa privacy", public_privacy_path, target: "_blank", rel: "noopener" %>
e i <%= link_to "termini di servizio", public_termini_path, target: "_blank", rel: "noopener" %>.
</span>
</label>
<p class="legal-accept-hint">
Registrandoti dichiari di essere maggiorenne e, se operi per una società sportiva con atleti minorenni,
di avere legittima facoltà (es. dirigente, coach autorizzato) e di rispettare le regole su immagini dei minori
descritte in privacy.
</p>
<%= f.submit "Continua", class: "btn btn-primary" %>
<% end %>
<p class="auth-footer">Hai già un account? <%= link_to "Accedi", public_login_path %></p>

View File

@@ -1,4 +1,6 @@
<% content_for :title, "Replay — #{@match.team.name} vs #{@match.opponent_name}" %>
<% content_for :meta_description, "Replay partita #{@match.team.name} vs #{@match.opponent_name} su Match Live TV." %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap">
<%= link_to "← Tutte le dirette", public_live_index_path, class: "back-link" %>

View File

@@ -1,4 +1,6 @@
<% content_for :title, "Accedi — Match Live TV" %>
<% content_for :meta_description, "Accesso area riservata per staff e società registrate su Match Live TV." %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>Accedi</h1>
@@ -7,7 +9,8 @@
<%= label_tag :email, "Email" %>
<%= email_field_tag :email, params[:email], required: true %>
<%= label_tag :password, "Password" %>
<%= password_field_tag :password, required: true %>
<%= password_field_tag :password, required: true, autocomplete: "current-password" %>
<p class="auth-forgot"><%= link_to "Password dimenticata?", public_password_forgot_path %></p>
<%= submit_tag "Entra", class: "btn btn-primary" %>
<% end %>
<p class="auth-footer">Nuovo? <%= link_to "Registrati", public_signup_path %></p>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<% @entries.each do |entry| %>
<url>
<loc><%= entry[:loc] %></loc>
<changefreq><%= entry[:changefreq] %></changefreq>
<priority><%= entry[:priority] %></priority>
</url>
<% end %>
</urlset>

View File

@@ -1,4 +1,5 @@
<% content_for :title, "Abbonamento — #{@team.name}" %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px">
<h1>Abbonamento</h1>

View File

@@ -1,4 +1,6 @@
<% content_for :title, "#{@team.name} — Dashboard" %>
<% content_for :meta_description, "Dashboard squadra #{@team.name} su Match Live TV." %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px">
<h1><%= @team.name %></h1>

View File

@@ -1,4 +1,5 @@
<% content_for :title, "Aggiungi staff — #{@team.name}" %>
<% content_for :robots, "noindex, nofollow" %>
<div class="wrap" style="padding-top:20px">
<h1>Aggiungi staff</h1>

View File

@@ -1,4 +1,6 @@
<% content_for :title, "Crea squadra — Match Live TV" %>
<% content_for :meta_description, "Configura la squadra dopo la registrazione su Match Live TV." %>
<% content_for :robots, "noindex, nofollow" %>
<section class="auth-page">
<h1>La tua squadra</h1>

View File

@@ -6,6 +6,7 @@
<div>
<%= link_to "Prezzi", public_prezzi_path %> ·
<%= link_to "Dirette", public_live_index_path %> ·
<%= link_to "FAQ", public_faq_path %> ·
<%= link_to "Privacy", public_privacy_path %> ·
<%= link_to "Termini", public_termini_path %>
</div>

View File

@@ -2,7 +2,10 @@
<div class="site-chrome">
<div class="site-masthead">
<div class="wrap mast-inner">
<%= link_to root_path, class: "brand" do %>Match <span>Live TV</span><% end %>
<%= link_to root_path, class: "mast-brand", aria: { label: "Match Live Tv" } do %>
<img class="mast-brand-logo" src="/logo.png?v=3" alt="" width="40" height="40" decoding="async">
<span class="brand">Match <span>Live TV</span></span>
<% end %>
<button type="button" class="nav-toggle" aria-label="Apri menu" aria-expanded="false" aria-controls="site-nav">
<span class="nav-toggle-bar" aria-hidden="true"></span>
@@ -17,6 +20,7 @@
<%= link_to "Home", root_path, class: (request.path == "/" ? "nav-active" : nil) %>
<%= link_to "Funzionalità", public_features_path, class: (request.path == "/funzionalita" ? "nav-active" : nil) %>
<%= link_to "Prezzi", public_prezzi_path, class: (request.path == "/prezzi" ? "nav-active" : nil) %>
<%= link_to "FAQ", public_faq_path, class: (request.path == "/faq" ? "nav-active" : nil) %>
<%= link_to "Dirette live", public_live_index_path, class: (live_section ? "nav-active" : nil) %>
<div class="nav-actions">
<% if logged_in? %>

View File

@@ -1,7 +1,19 @@
<% description = content_for?(:meta_description) ? yield(:meta_description) : "Trasmetti le partite dal telefono. Parenti e nonni guardano dal browser. Se te la perdi, la ritrovi nell'archivio." %>
<% description = content_for?(:meta_description) ? yield(:meta_description) : "Diretta live delle partite giovanili dal telefono: genitori e nonni guardano dal browser, senza app. Archivio partite per non perderle." %>
<% page_title = content_for?(:title) ? yield(:title) : "Match Live TV — Diretta live partite giovanili" %>
<meta name="description" content="<%= description %>">
<meta property="og:title" content="<%= content_for?(:title) ? yield(:title) : 'Match Live TV' %>">
<meta name="robots" content="<%= seo_robots_content %>">
<link rel="canonical" href="<%= seo_canonical_url %>">
<meta property="og:site_name" content="Match Live TV">
<meta property="og:title" content="<%= page_title %>">
<meta property="og:description" content="<%= description %>">
<meta property="og:type" content="website">
<meta property="og:url" content="<%= request.original_url %>">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<meta property="og:url" content="<%= seo_canonical_url %>">
<meta property="og:locale" content="it_IT">
<meta property="og:image" content="<%= seo_og_image_url %>">
<meta property="og:image:alt" content="Match Live TV — diretta live partite giovanili da smartphone">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<%= page_title %>">
<meta name="twitter:description" content="<%= description %>">
<meta name="twitter:image" content="<%= seo_og_image_url %>">
<link rel="icon" href="/favicon-32.png?v=3" type="image/png" sizes="32x32">
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=3" sizes="180x180">

View File

@@ -0,0 +1,11 @@
<p>Ciao <%= @user.name %>,</p>
<p>Hai richiesto di reimpostare la password del tuo account Match Live TV.</p>
<p>
<a href="<%= @reset_url %>">Clicca qui per scegliere una nuova password</a>
</p>
<p>Il link è valido per <%= @expiry_hours %> ore. Se non hai richiesto tu il reset, ignora questa email: la password non verrà modificata.</p>
<p style="color:#666;font-size:12px">Match Live TV — <%= MatchLiveTv.privacy_controller_email %></p>

View File

@@ -0,0 +1,10 @@
Ciao <%= @user.name %>,
Hai richiesto di reimpostare la password del tuo account Match Live TV.
Apri questo link (valido <%= @expiry_hours %> ore):
<%= @reset_url %>
Se non hai richiesto tu il reset, ignora questa email.
Match Live TV — <%= MatchLiveTv.privacy_controller_email %>

View File

@@ -31,13 +31,11 @@ Rails.application.configure do
# Store uploaded files on the local file system (see config/storage.yml for options).
config.active_storage.service = :local
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
# Disable caching for Action Mailer templates even if Action Controller
# caching is enabled.
config.action_mailer.raise_delivery_errors = true
config.action_mailer.perform_caching = false
config.action_mailer.delivery_method = :file
config.action_mailer.file_settings = { location: Rails.root.join("tmp", "mail") }
config.action_mailer.perform_deliveries = true
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
# Print deprecation notices to the Rails logger.

View File

@@ -42,6 +42,23 @@ Rails.application.configure do
config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")
config.action_mailer.perform_caching = false
config.action_mailer.default_url_options = {
host: URI.parse(ENV.fetch("APP_PUBLIC_URL", "https://matchlivetv.eminux.it")).host,
protocol: URI.parse(ENV.fetch("APP_PUBLIC_URL", "https://matchlivetv.eminux.it")).scheme
}
config.action_mailer.raise_delivery_errors = true
if ENV["SMTP_ADDRESS"].present?
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV["SMTP_ADDRESS"],
port: ENV.fetch("SMTP_PORT", 587).to_i,
user_name: ENV["SMTP_USERNAME"],
password: ENV["SMTP_PASSWORD"],
authentication: ENV.fetch("SMTP_AUTH", "plain"),
enable_starttls_auto: ENV.fetch("SMTP_STARTTLS", "true") == "true"
}
end
config.i18n.fallbacks = true
config.active_support.report_deprecations = false
config.active_record.dump_schema_after_migration = false

View File

@@ -51,5 +51,29 @@ module MatchLiveTv
def stripe_enabled?
stripe_secret_key.present?
end
def privacy_controller_name
ENV.fetch("PRIVACY_CONTROLLER_NAME", "Gestore del servizio Match Live TV")
end
def privacy_controller_email
ENV.fetch("PRIVACY_CONTACT_EMAIL", "privacy@matchlivetv.it")
end
def privacy_controller_address
ENV.fetch("PRIVACY_CONTROLLER_ADDRESS", "Indirizzo del titolare — configurare PRIVACY_CONTROLLER_ADDRESS")
end
def privacy_controller_vat
ENV["PRIVACY_CONTROLLER_VAT"].presence
end
def mail_from
ENV.fetch("MAILER_FROM", "Match Live TV <noreply@matchlivetv.it>")
end
def password_reset_expiry_hours
ENV.fetch("PASSWORD_RESET_EXPIRY_HOURS", "2").to_i
end
end
end

View File

@@ -73,9 +73,12 @@ Rails.application.routes.draw do
get "replay/:id", to: "public/replay#show", as: :public_replay
root to: "public/pages#home"
get "sitemap.xml", to: "public/sitemap#show", defaults: { format: :xml }
scope module: :public, as: :public do
get "funzionalita", to: "pages#features", as: :features
get "faq", to: "pages#faq", as: :faq
get "pallavolo-giovanile", to: "pages#pallavolo", as: :pallavolo
get "prezzi", to: "pages#pricing", as: :prezzi
get "pricing", to: redirect("/prezzi")
get "privacy", to: "pages#privacy", as: :privacy
@@ -85,6 +88,10 @@ Rails.application.routes.draw do
get "login", to: "sessions#new"
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
get "password/forgot", to: "password_resets#new", as: :password_forgot
post "password/forgot", to: "password_resets#create"
get "password/reset", to: "password_resets#edit", as: :password_reset
patch "password/reset", to: "password_resets#update"
get "teams/new", to: "teams#new"
post "teams", to: "teams#create"
get "teams/:id/dashboard", to: "teams#dashboard", as: :team_dashboard

View File

@@ -0,0 +1,7 @@
class AddPasswordResetToUsers < ActiveRecord::Migration[7.2]
def change
add_column :users, :password_reset_digest, :string
add_column :users, :password_reset_sent_at, :datetime
add_index :users, :password_reset_digest, unique: true
end
end

5
backend/db/schema.rb generated
View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2026_05_27_130000) do
ActiveRecord::Schema[7.2].define(version: 2026_05_27_140000) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@@ -192,7 +192,10 @@ ActiveRecord::Schema[7.2].define(version: 2026_05_27_130000) do
t.string "role", default: "coach", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "password_reset_digest"
t.datetime "password_reset_sent_at"
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["password_reset_digest"], name: "index_users_on_password_reset_digest", unique: true
end
create_table "youtube_credentials", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -206,6 +206,116 @@
margin: -12px 0 20px;
}
.live-main .section-heading {
font-size: 1rem;
font-weight: 700;
color: #ccc;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 0 0 12px;
}
.live-main .section-heading--spaced {
margin-top: 32px;
}
.live-main .badge-scheduled {
background: #1565c0;
color: #fff;
}
.live-main .live-card--upcoming {
border-color: #2a3a4a;
}
.live-main .live-card--upcoming:hover {
border-color: #1565c0;
transform: none;
}
.live-main .upcoming-when {
font-size: 1rem;
font-weight: 600;
color: #9ecbff;
margin: 0 0 12px;
}
.live-main .empty-state--soft {
margin-top: 28px;
padding: 28px 24px;
text-align: left;
}
.live-main .empty-state--soft p {
margin: 0 0 10px;
color: #bbb;
}
.live-main .empty-hero {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 24px;
margin-top: 8px;
align-items: start;
}
@media (max-width: 860px) {
.live-main .empty-hero {
grid-template-columns: 1fr;
}
}
.live-main .empty-hero-copy h2 {
font-size: 1.35rem;
margin: 0 0 12px;
color: #fff;
}
.live-main .empty-hero-copy > p {
color: #aaa;
margin: 0 0 16px;
line-height: 1.5;
}
.live-main .empty-hero-list {
margin: 0 0 20px;
padding-left: 1.2rem;
color: #aaa;
line-height: 1.6;
}
.live-main .empty-hero-cta {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 0;
}
.live-main .demo-live-card {
background: #14141c;
border: 1px dashed #333;
border-radius: 14px;
padding: 16px;
}
.live-main .demo-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #888;
margin: 0 0 12px;
}
.live-main .live-card--demo {
opacity: 0.92;
pointer-events: none;
}
.live-main .btn-watch--demo {
cursor: default;
opacity: 0.85;
}
.live-main .replay-video {
width: 100%;
border-radius: 12px;

BIN
backend/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -22,13 +22,32 @@ a:hover { text-decoration: underline; }
.mast-inner {
display: flex;
align-items: center;
justify-content: space-between;
justify-content: flex-start;
gap: 16px;
padding-top: 14px;
padding-bottom: 14px;
}
.brand { font-weight: 800; font-size: 1.15rem; letter-spacing: 0.03em; color: #fff; text-decoration: none; flex-shrink: 0; }
.mast-inner .nav-toggle { margin-left: auto; }
.brand { font-weight: 800; font-size: 1.15rem; letter-spacing: 0.03em; color: #fff; flex-shrink: 0; }
.brand span { color: #e53935; }
.mast-brand {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
text-decoration: none;
line-height: 1;
}
.mast-brand:hover { opacity: 0.92; text-decoration: none; }
.mast-brand:hover .brand { color: #fff; }
.mast-brand-logo {
display: block;
width: 40px;
height: 40px;
border-radius: 8px;
object-fit: contain;
flex-shrink: 0;
}
.nav-toggle {
display: none;
flex-direction: column;
@@ -485,10 +504,99 @@ body.nav-menu-open { overflow: hidden; }
.compare-table th, .compare-table td { padding: 10px 12px; border-bottom: 1px solid #2a2a36; text-align: left; }
.compare-table th { color: #aaa; font-weight: 600; }
.card { background: #14141c; border: 1px solid #2a2a36; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.seo-prose { max-width: 720px; margin: 0 auto; color: #bbb; line-height: 1.65; }
.seo-prose h2 { color: #fff; font-size: 1.25rem; margin: 28px 0 12px; }
.seo-prose h2:first-child { margin-top: 0; }
.seo-prose p { margin: 0 0 14px; }
.seo-prose ul { margin: 0 0 16px; padding-left: 1.25rem; }
.seo-prose a { color: #e53935; }
.seo-page .seo-lead { color: #aaa; max-width: 640px; line-height: 1.55; margin-bottom: 28px; }
.faq-list { max-width: 720px; margin: 0 auto; }
.faq-item {
background: #14141c;
border: 1px solid #2a2a36;
border-radius: 12px;
margin-bottom: 10px;
padding: 0 18px;
}
.faq-item summary {
cursor: pointer;
font-weight: 600;
color: #fff;
padding: 16px 0;
list-style: none;
}
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item summary::after {
content: "+";
float: right;
color: #e53935;
font-weight: 700;
}
.faq-item[open] summary::after { content: ""; }
.faq-item p { color: #aaa; margin: 0 0 16px; line-height: 1.55; font-size: 0.95rem; }
.legal-doc { max-width: 760px; padding-top: 24px; padding-bottom: 48px; color: #bbb; line-height: 1.65; font-size: 0.95rem; }
.legal-doc h1 { color: #fff; font-size: 1.6rem; margin-bottom: 8px; }
.legal-doc h2 { color: #fff; font-size: 1.1rem; margin: 28px 0 10px; }
.legal-doc p { margin: 0 0 12px; }
.legal-doc ul { margin: 0 0 14px; padding-left: 1.25rem; }
.legal-doc a { color: #e53935; }
.legal-meta { color: #888; font-size: 0.88rem; margin-bottom: 24px; }
.legal-back { margin-top: 32px; }
.legal-table { width: 100%; border-collapse: collapse; margin: 12px 0 16px; font-size: 0.88rem; }
.legal-table th, .legal-table td { border: 1px solid #2a2a36; padding: 10px 12px; text-align: left; vertical-align: top; }
.legal-table th { background: #14141c; color: #ccc; }
.legal-accept {
display: flex;
gap: 12px;
align-items: flex-start;
margin: 20px 0 10px;
font-size: 0.9rem;
color: #c8c8d4;
line-height: 1.5;
cursor: pointer;
}
.legal-accept input[type="checkbox"] {
width: 18px;
height: 18px;
min-width: 18px;
margin: 3px 0 0;
flex-shrink: 0;
accent-color: #e53935;
cursor: pointer;
}
.legal-accept-text { flex: 1; min-width: 0; }
.legal-accept-text a { color: #e53935; text-decoration: underline; }
.legal-accept-hint {
font-size: 0.82rem;
color: #9a9aaa;
margin: 0 0 18px;
line-height: 1.5;
padding-left: 30px;
}
.auth-forgot { margin: 4px 0 14px; font-size: 0.88rem; text-align: right; }
.auth-forgot a { color: #e53935; }
table.data { width: 100%; border-collapse: collapse; }
table.data th, table.data td { padding: 8px; border-bottom: 1px solid #2a2a36; text-align: left; }
input, select { width: 100%; padding: 10px 12px; margin-bottom: 12px; border-radius: 8px; border: 1px solid #333; background: #0a0a0e; color: #fff; }
label { display: block; margin-bottom: 4px; font-size: 0.88rem; color: #999; }
input, select {
width: 100%;
padding: 12px 14px;
margin-bottom: 12px;
border-radius: 8px;
border: 1px solid #3d3d4a;
background: #1c1c26;
color: #fff;
font-size: 1rem;
}
input:focus, select:focus {
outline: none;
border-color: #e53935;
box-shadow: 0 0 0 2px rgba(229, 57, 53, 0.25);
}
input::placeholder { color: #666; }
label { display: block; margin-bottom: 6px; font-size: 0.9rem; color: #c8c8d4; font-weight: 500; }
.auth-page {
display: flex;
flex-direction: column;
@@ -498,13 +606,23 @@ label { display: block; margin-bottom: 4px; font-size: 0.88rem; color: #999; }
padding: 48px 20px 64px;
text-align: center;
}
.auth-page h1 { margin: 0 0 8px; }
.auth-page .auth-lead { color: #aaa; margin: 0 0 24px; max-width: 420px; }
.auth-page h1 { margin: 0 0 8px; color: #fff; }
.auth-page .auth-lead { color: #b8b8c4; margin: 0 0 24px; max-width: 480px; line-height: 1.5; }
.auth-page .card {
width: 100%;
max-width: 420px;
max-width: 440px;
margin: 0;
padding: 28px 26px;
text-align: left;
background: #16161e;
border: 1px solid #3a3a48;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
}
.auth-page .card form .btn-primary {
width: 100%;
margin-top: 8px;
padding: 14px;
font-size: 1rem;
}
.auth-page .card-wide { max-width: 480px; }
.auth-page .auth-footer { margin-top: 16px; font-size: 0.92rem; color: #aaa; }
.auth-page .auth-footer { margin-top: 20px; font-size: 0.92rem; color: #aaa; text-align: center; }

BIN
backend/public/og-share.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -1 +1,24 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
# Match Live TV — https://matchlivetv.eminux.it
User-agent: *
Allow: /
Allow: /live
Allow: /funzionalita
Allow: /prezzi
Allow: /faq
Allow: /pallavolo-giovanile
Disallow: /admin
Disallow: /login
Disallow: /signup
Disallow: /logout
Disallow: /password/
Disallow: /teams/
Disallow: /join/
Disallow: /api/
Disallow: /webhooks/
Disallow: /internal/
Disallow: /cable
Disallow: /hls/
Disallow: /replay/
Sitemap: https://matchlivetv.eminux.it/sitemap.xml