Initial commit: monorepo Match Live TV.

Rails API, app Flutter, infrastruttura Docker/MediaMTX, sito marketing e documentazione di deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-26 17:45:37 +02:00
commit bba6df52c0
381 changed files with 20599 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
<h2>Sessioni attive</h2>
<% if @active_sessions.any? %>
<table>
<thead><tr><th>Match</th><th>Status</th><th>Iniziata</th><th></th></tr></thead>
<tbody>
<% @active_sessions.each do |s| %>
<tr>
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
<td><span class="badge live"><%= s.status %></span></td>
<td><%= s.started_at %></td>
<td><%= link_to "Dettaglio", admin_session_path(s) %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>Nessuna sessione attiva.</p>
<% end %>
<h2>Squadre</h2>
<ul>
<% @teams.each do |t| %>
<li><%= link_to t.name, admin_team_path(t) %> — <%= t.matches.count %> partite</li>
<% end %>
</ul>

View File

@@ -0,0 +1,14 @@
<h2>Stream Sessions</h2>
<table>
<thead><tr><th>Match</th><th>Status</th><th>Disconnects</th><th></th></tr></thead>
<tbody>
<% @sessions.each do |s| %>
<tr>
<td><%= s.match.team.name %> vs <%= s.match.opponent_name %></td>
<td><span class="badge <%= s.live? ? 'live' : 'ended' %>"><%= s.status %></span></td>
<td><%= s.disconnection_count %></td>
<td><%= link_to "Dettaglio", admin_session_path(s) %></td>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -0,0 +1,21 @@
<h2>Session <%= @session.id %></h2>
<p>Status: <strong><%= @session.status %></strong></p>
<p>Match: <%= @session.match.team.name %> vs <%= @session.match.opponent_name %></p>
<% if @session.youtube_broadcast_id %>
<p>YouTube: <a href="https://studio.youtube.com/video/<%= @session.youtube_broadcast_id %>/livestreaming" target="_blank">Broadcast</a></p>
<% end %>
<p>RTMP ingest: <code><%= @session.rtmp_ingest_url %></code></p>
<h3>Eventi</h3>
<table>
<thead><tr><th>Tipo</th><th>Quando</th><th>Meta</th></tr></thead>
<tbody>
<% @events.each do |e| %>
<tr>
<td><%= e.event_type %></td>
<td><%= e.occurred_at %></td>
<td><%= e.metadata.to_json %></td>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -0,0 +1,13 @@
<h2>Teams</h2>
<table>
<thead><tr><th>Nome</th><th>Sport</th><th>YouTube</th></tr></thead>
<tbody>
<% @teams.each do |t| %>
<tr>
<td><%= link_to t.name, admin_team_path(t) %></td>
<td><%= t.sport %></td>
<td><%= t.youtube_credential.present? ? "✓" : "—" %></td>
</tr>
<% end %>
</tbody>
</table>

View File

@@ -0,0 +1,10 @@
<h2><%= @team.name %></h2>
<p>Sport: <%= @team.sport %></p>
<p>YouTube: <%= @team.youtube_credential ? "Connesso" : link_to("Connetti", "/api/v1/teams/#{@team.id}/youtube/authorize") %></p>
<h3>Partite</h3>
<ul>
<% @matches.each do |m| %>
<li><%= m.opponent_name %> — <%= m.scheduled_at %></li>
<% end %>
</ul>

View File

@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<title>Match Live TV Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root { --red: #FF2D2D; --bg: #0A0A0A; --card: #1E1E1E; --text: #fff; }
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); margin: 0; padding: 1rem 2rem; }
a { color: var(--red); }
header { border-bottom: 1px solid #333; padding-bottom: 1rem; margin-bottom: 2rem; }
h1 span { color: var(--red); }
table { width: 100%; border-collapse: collapse; background: var(--card); border-radius: 8px; overflow: hidden; }
th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #333; }
.badge { display: inline-block; padding: 0.2rem 0.6rem; border-radius: 4px; font-size: 0.8rem; }
.live { background: var(--red); }
.ended { background: #555; }
nav a { margin-right: 1rem; }
</style>
</head>
<body>
<header>
<h1>MATCH <span>LIVE</span> TV — Admin</h1>
<nav>
<%= link_to "Dashboard", admin_root_path %>
<%= link_to "Teams", admin_teams_path %>
<%= link_to "Sessions", admin_sessions_path %>
</nav>
</header>
<%= yield %>
</body>
</html>

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
/* Email styles need to be inline */
</style>
</head>
<body>
<%= yield %>
</body>
</html>

View File

@@ -0,0 +1 @@
<%= yield %>

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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=14">
</head>
<body>
<%= render "shared/marketing_nav" %>
<main>
<% if flash[:notice] %><div class="wrap"><div class="flash notice"><%= flash[:notice] %></div></div><% end %>
<% if flash[:alert] %><div class="wrap"><div class="flash alert"><%= flash[:alert] %></div></div><% end %>
<%= yield %>
</main>
<%= render "shared/marketing_footer" %>
</body>
</html>

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<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=14">
<link rel="stylesheet" href="/live.css">
<%= yield :head %>
</head>
<body>
<%= render "shared/marketing_nav" %>
<main class="live-main">
<%= yield %>
</main>
<%= render "shared/marketing_footer" %>
</body>
</html>

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= content_for?(:title) ? yield(:title) : "Match Live TV" %></title>
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; background: #0f0f12; color: #f5f5f5; line-height: 1.5; }
a { color: #ff6b6b; }
.header { border-bottom: 1px solid #2a2a32; background: #14141a; padding: 14px 0; margin-bottom: 24px; }
.wrap { max-width: 880px; margin: 0 auto; padding: 0 16px 40px; }
.header .wrap { display: flex; justify-content: space-between; align-items: center; }
.brand { font-weight: 800; color: #fff; text-decoration: none; }
.brand span { color: #e53935; }
.flash { padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; }
.flash.notice { background: #1b3d2a; color: #a5f0b8; }
.flash.alert { background: #3d1b1b; color: #ffb4b4; }
.card { background: #1a1a22; border: 1px solid #2a2a32; border-radius: 12px; padding: 20px; margin-bottom: 16px; }
.btn { display: inline-block; padding: 12px 20px; border-radius: 8px; font-weight: 700; text-decoration: none; border: none; cursor: pointer; }
.btn-primary { background: #e53935; color: #fff; }
.btn-secondary { background: #2a2a32; color: #fff; }
label { display: block; margin-bottom: 6px; font-size: 0.9rem; color: #aaa; }
input, select { width: 100%; padding: 10px 12px; margin-bottom: 14px; border-radius: 8px; border: 1px solid #333; background: #0f0f12; color: #fff; }
.plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
.plan-premium { border-color: #e53935; }
ul.features { padding-left: 18px; color: #ccc; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 8px; border-bottom: 1px solid #2a2a32; }
</style>
</head>
<body>
<header class="header">
<div class="wrap">
<%= link_to public_pricing_path, class: "brand" do %>Match <span>Live TV</span><% end %>
<nav>
<%= link_to "Prezzi", public_pricing_path %>
<% if logged_in? %>
<% if current_user.teams.any? %>
· <%= link_to "Dashboard", public_team_dashboard_path(current_user.teams.first) %>
<% end %>
· <%= button_to "Esci", public_logout_path, method: :delete, form: { style: "display:inline" }, class: "btn btn-secondary", style: "padding:6px 12px;font-size:0.85rem" %>
<% else %>
· <%= link_to "Accedi", public_login_path %>
· <%= link_to "Registrati", public_signup_path, class: "btn btn-primary", style: "padding:6px 12px;font-size:0.85rem" %>
<% end %>
</nav>
</div>
</header>
<main class="wrap">
<% if flash[:notice] %><div class="flash notice"><%= flash[:notice] %></div><% end %>
<% if flash[:alert] %><div class="flash alert"><%= flash[:alert] %></div><% end %>
<%= yield %>
</main>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<% content_for :title, "Invito squadra" %>
<div class="card" style="max-width:480px">
<h1>Accesso staff — <%= @invitation.team.name %></h1>
<p>Sei stato aggiunto allo staff come <strong><%= @invitation.staff_kind == "regia" ? "Regia" : "Trasmissione" %></strong> (<%= @invitation.email %>).</p>
<% if logged_in? %>
<%= button_to "Accetta invito", public_invitation_path(token: @token), method: :post, class: "btn btn-primary" %>
<% else %>
<p><%= link_to "Accedi", public_login_path %> o <%= link_to "registrati", public_signup_path %> con <%= @invitation.email %>.</p>
<%= button_to "Accetta (se già loggato)", public_invitation_path(token: @token), method: :post, class: "btn btn-secondary" %>
<% end %>
</div>

View File

@@ -0,0 +1,76 @@
<% content_for :title do %>Dirette in corso — Match Live TV<% end %>
<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? %>
<p class="results-hint">
Risultati per «<%= h @query %>»: <%= @sessions.size %> diretta<%= @sessions.size == 1 ? "" : "e" %>
</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">
<% 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>
<% end %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,227 @@
<% content_for :title do %><%= @match.team.name %> vs <%= @match.opponent_name %> — Match Live TV<% end %>
<% content_for :head do %>
<% unless @stream_closed %>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7"></script>
<% end %>
<% end %>
<div class="wrap">
<%= link_to "← Tutte le dirette", public_live_index_path, class: "back-link" %>
<h1><%= @match.team.name %> vs <%= @match.opponent_name %></h1>
<p>
<% if @stream_closed %>
<span id="status-badge" class="badge badge-ended">Diretta chiusa</span>
<% elsif @on_air %>
<span id="status-badge" class="badge badge-on-air">In onda</span>
<% else %>
<span id="status-badge" class="badge badge-wait">In attesa</span>
<% end %>
</p>
<div id="scoreboard" class="scoreboard">
<p id="sets-line" class="sets-line"><%= live_score_sets_label(@session.score_state) || "—" %></p>
<p id="partials-line" class="partials-line"<%= " hidden" unless live_score_partials_label(@session.score_state).present? %>>
Parziali: <%= live_score_partials_label(@session.score_state) %>
</p>
<p id="score-line" class="score"><%= live_score_points_label(@match, @session.score_state) %></p>
</div>
<% if @stream_closed %>
<div id="stream-ended" class="stream-ended" role="status" aria-live="polite">
<div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2>Diretta terminata</h2>
<p>Lo streaming di questa partita è stato chiuso.</p>
<% if @session.ended_at %>
<p class="stream-ended-meta">Chiusa il <%= l(@session.ended_at.in_time_zone, format: :long) %></p>
<% end %>
<p class="stream-ended-hint">Il punteggio finale resta visibile sopra.</p>
<%= link_to "Tutte le dirette", public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
</div>
<% else %>
<video id="player" controls playsinline muted autoplay></video>
<p id="offline-msg" style="display:none;color:#aaa;">La diretta non è ancora disponibile. Si aggiorna automaticamente quando torna in onda.</p>
<script>
const sessionId = "<%= @session.id %>";
const hlsUrl = "<%= j @session.hls_playback_url %>";
const video = document.getElementById("player");
const badge = document.getElementById("status-badge");
const setsLine = document.getElementById("sets-line");
const partialsLine = document.getElementById("partials-line");
const scoreLine = document.getElementById("score-line");
const offlineMsg = document.getElementById("offline-msg");
let hls = null;
let wasOnAir = <%= @on_air ? "true" : "false" %>;
let reloadTimer = null;
let stallPolls = 0;
let streamClosed = false;
function setBadge(status, live, onAir, closed) {
if (closed) {
badge.textContent = "Diretta chiusa";
badge.className = "badge badge-ended";
return;
}
if (onAir) {
badge.textContent = "In onda";
badge.className = "badge badge-on-air";
} else if (live) {
badge.textContent = "LIVE";
badge.className = "badge badge-live";
} else if (status === "reconnecting") {
badge.textContent = "Riconnessione";
badge.className = "badge badge-wait";
} else {
badge.textContent = status;
badge.className = "badge badge-wait";
}
}
function formatSets(score) {
if (!score) return "—";
return `Set ${score.current_set} · Set vinti ${score.home_sets}-${score.away_sets}`;
}
function formatPartials(score) {
const partials = score?.set_partials;
if (!partials?.length) return "";
return partials
.map((p) => `Set ${p.set} ${p.home}-${p.away}`)
.join(" · ");
}
function formatPoints(data) {
if (!data.score || !data.home_name || !data.away_name) return "—";
const s = data.score;
return `${data.home_name} ${s.home_points} - ${s.away_points} ${data.away_name}`;
}
function updateScoreboard(data) {
const score = data.score;
setsLine.textContent = formatSets(score);
const partials = formatPartials(score);
if (partials) {
partialsLine.textContent = `Parziali: ${partials}`;
partialsLine.hidden = false;
} else {
partialsLine.hidden = true;
}
scoreLine.textContent = formatPoints(data);
}
function showStreamEnded() {
if (streamClosed) return;
streamClosed = true;
destroyPlayer();
video.style.display = "none";
offlineMsg.style.display = "none";
const panel = document.createElement("div");
panel.id = "stream-ended";
panel.className = "stream-ended";
panel.setAttribute("role", "status");
panel.innerHTML = `
<div class="stream-ended-icon" aria-hidden="true">📡</div>
<h2>Diretta terminata</h2>
<p>Lo streaming di questa partita è stato chiuso.</p>
<p class="stream-ended-hint">Il punteggio finale resta visibile sopra.</p>
<a href="<%= public_live_index_path %>" class="btn btn-secondary stream-ended-btn">Tutte le dirette</a>
`;
video.parentNode.insertBefore(panel, video.nextSibling);
}
function hlsUrlFresh() {
const sep = hlsUrl.includes("?") ? "&" : "?";
return `${hlsUrl}${sep}_ts=${Date.now()}`;
}
function destroyPlayer() {
if (hls) {
hls.destroy();
hls = null;
}
video.removeAttribute("src");
video.load();
}
function startPlayer() {
destroyPlayer();
const url = hlsUrlFresh();
if (Hls.isSupported()) {
hls = new Hls({ lowLatencyMode: true, enableWorker: true });
hls.loadSource(url);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => video.play().catch(() => {}));
hls.on(Hls.Events.ERROR, (_, data) => {
if (data.fatal) {
scheduleReload(2500);
}
});
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
video.addEventListener("loadedmetadata", () => video.play().catch(() => {}), { once: true });
}
}
function scheduleReload(delayMs) {
if (reloadTimer) return;
reloadTimer = setTimeout(() => {
reloadTimer = null;
pollStatus();
}, delayMs);
}
async function pollStatus() {
try {
const res = await fetch(`/live/${sessionId}/status.json`);
const data = await res.json();
setBadge(data.status, data.live, data.on_air, data.stream_closed);
updateScoreboard(data);
if (data.stream_closed) {
showStreamEnded();
return;
}
const onAir = !!data.on_air;
if (onAir) {
offlineMsg.style.display = "none";
if (!wasOnAir) {
startPlayer();
stallPolls = 0;
} else if (video.readyState < 2) {
stallPolls += 1;
if (stallPolls >= 2) {
startPlayer();
stallPolls = 0;
}
} else {
stallPolls = 0;
}
} else {
offlineMsg.style.display = "block";
destroyPlayer();
stallPolls = 0;
}
wasOnAir = onAir;
} catch (_) {
scheduleReload(5000);
}
}
if (wasOnAir) {
startPlayer();
} else {
offlineMsg.style.display = "block";
}
pollStatus();
setInterval(pollStatus, 3000);
</script>
<% end %>
</div>

View File

@@ -0,0 +1,63 @@
<% 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." %>
<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>
<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>
</div>
<div class="feature-card">
<h3>Revoca accessi</h3>
<p>L'owner rimuove membri dello staff e annulla richieste pendenti dalla dashboard web.</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>
</div>
</div>
</section>
<section class="section">
<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>
</div>
<div class="feature-card">
<h3>Regia separata</h3>
<p>Secondo dispositivo per aggiornare il punteggio via QR pairing.</p>
</div>
<div class="feature-card">
<h3>Pagina pubblica /live</h3>
<p>Genitori e tifosi guardano dal browser senza installare nulla.</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>
</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>
</div>
<div class="feature-card">
<h3>YouTube della società</h3>
<p>Premium Full: relay verso il canale YouTube 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>
</div>
</div>
</section>
<p style="text-align:center;margin:40px 0">
<%= link_to "Registra la squadra", public_signup_path, class: "btn btn-primary" %>
</p>
</div>

View File

@@ -0,0 +1,88 @@
<% 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." %>
<section class="hero hero-split">
<div class="wrap hero-grid">
<div class="hero-content">
<p class="hero-live-badge"><span class="hero-live-dot" aria-hidden="true"></span> LIVE</p>
<h1>Ogni partita, ogni evento, per chi <span class="hero-accent">non può esserci.</span></h1>
<p class="tagline">
Trasmetti in diretta dallo smartphone. I nonni, i parenti lontani, gli amici —
guardano dal browser, senza installare nulla.
E se te la sei persa, la ritrovi nell'archivio.
</p>
<div class="hero-cta">
<%= link_to "Registra la tua squadra", public_signup_path, class: "btn btn-primary" %>
<%= link_to "Guarda le dirette", public_live_index_path, class: "btn btn-secondary" %>
</div>
<ul class="hero-features" aria-label="Vantaggi principali">
<li class="hero-feature-item">
<span class="hero-feature-icon" aria-hidden="true"><i class="fa-solid fa-tower-broadcast"></i></span>
<span class="hero-feature-label">Dirette stabili</span>
</li>
<li class="hero-feature-item">
<span class="hero-feature-icon" aria-hidden="true"><i class="fa-solid fa-cloud"></i></span>
<span class="hero-feature-label">Archivio sicuro</span>
</li>
<li class="hero-feature-item">
<span class="hero-feature-icon" aria-hidden="true"><i class="fa-solid fa-user-group"></i></span>
<span class="hero-feature-label">Condividi con chi vuoi</span>
</li>
<li class="hero-feature-item hero-feature-item--last">
<span class="hero-feature-icon" aria-hidden="true"><i class="fa-solid fa-mobile-screen"></i></span>
<span class="hero-feature-label">Tutto dal tuo telefono</span>
</li>
</ul>
</div>
<div class="hero-visual">
<%= image_tag "/hero-devices.png", alt: "Smartphone su treppiede per la diretta e secondo telefono per il punteggio in palestra", class: "hero-devices-img", loading: "eager", fetchpriority: "high" %>
</div>
</div>
</section>
<section class="section wrap">
<h2>Come funziona</h2>
<div class="steps">
<div class="step step--visual">
<div class="step-visual">
<%= image_tag "/home-step-registra-squadra.png?v=1", alt: "Registrazione squadra: scudo, pallone e modulo con email, password e nome squadra", class: "step-visual-img", loading: "lazy" %>
</div>
<div class="step-num">01</div>
<h3>Registra la squadra</h3>
<p>La società si iscrive sul sito e sceglie il piano più adatto.</p>
</div>
<div class="step step--visual">
<div class="step-visual">
<%= image_tag "/home-step-invita-trasmette.png?v=1", alt: "Invito a trasmettere: smartphone con invio email e busta con accetta invito", class: "step-visual-img", loading: "lazy" %>
</div>
<div class="step-num">02</div>
<h3>Invita chi trasmette</h3>
<p>Coach e volontari ricevono un invito: ognuno accede con la propria email.</p>
</div>
<div class="step step--visual">
<div class="step-visual">
<%= image_tag "/home-step-vai-in-diretta.png?v=1", alt: "Vai in diretta: smartphone su treppiede, punteggio live e condivisione link", class: "step-visual-img", loading: "lazy" %>
</div>
<div class="step-num">03</div>
<h3>Vai in diretta</h3>
<p>Dal telefono si avvia la partita: punteggio aggiornato e link da condividere con le famiglie.</p>
</div>
<div class="step step--visual">
<div class="step-visual">
<%= image_tag "/home-step-tutti-guardano.png?v=2", alt: "Tutti guardano da casa: archivio partite, partita salvata e link per genitori e parenti", class: "step-visual-img", loading: "lazy" %>
</div>
<div class="step-num">04</div>
<h3>Tutti guardano da casa</h3>
<p>Genitori, nonni e parenti aprono il link nel browser. La partita resta anche in archivio.</p>
</div>
</div>
</section>
<section class="section wrap plans-teaser">
<h2>Piani per ogni esigenza</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>

View File

@@ -0,0 +1,72 @@
<% content_for :title, "Prezzi — Match Live TV" %>
<div class="wrap" style="padding-top:24px">
<h1>Piani per la tua squadra</h1>
<p style="color:#aaa">Abbonamento annuale per squadra: lo staff gestisce trasmissione e regia con credenziali dedicate. Pagamento sicuro su Stripe.</p>
<div class="plans">
<div class="plan-card">
<h3>Free</h3>
<div class="plan-price">€0</div>
<ul>
<li><strong>1</strong> persona staff / anno per la <strong>trasmissione</strong></li>
<li><strong>1</strong> persona staff / anno per la <strong>regia</strong></li>
<li><strong>1</strong> live alla volta su matchlivetv.eminux.it</li>
<li>Punteggio live e regia da app</li>
<li style="color:#888">No salvataggio / replay</li>
<li style="color:#888">No YouTube</li>
</ul>
<%= link_to "Inizia gratis", public_signup_path, class: "btn btn-secondary" %>
</div>
<div class="plan-card featured">
<h3>Premium Light</h3>
<div class="plan-price" style="color:#e53935">€40<span style="font-size:1rem;font-weight:400">/anno</span></div>
<p style="color:#aaa;font-size:0.9rem;margin:-8px 0 12px">oppure <strong>€5</strong>/mese</p>
<ul>
<li><strong>5</strong> persone staff / anno per la <strong>trasmissione</strong></li>
<li><strong>5</strong> persone staff / anno per la <strong>regia</strong></li>
<li>Fino a <strong>3</strong> partite in contemporanea</li>
<li>Live su matchlivetv.eminux.it e sul <strong>canale YouTube Match TV Light</strong></li>
<li>Archivio server <strong>30 giorni</strong> e download su telefono</li>
</ul>
<%= link_to "Registrati", public_signup_path, class: "btn btn-primary" %>
</div>
<div class="plan-card">
<h3>Premium Full</h3>
<div class="plan-price">€200<span style="font-size:1rem;font-weight:400">/anno</span></div>
<p style="color:#aaa;font-size:0.9rem;margin:-8px 0 12px">oppure <strong>€20</strong>/mese</p>
<ul>
<li>Staff <strong>illimitato</strong> / anno (trasmissione e regia)</li>
<li>Fino a <strong>10</strong> partite in contemporanea</li>
<li>Collegamento con il <strong>canale YouTube della società</strong></li>
<li>Archivio server <strong>90 giorni</strong> e download su telefono</li>
<li>Sito società (in arrivo)</li>
</ul>
<%= link_to "Registrati", public_signup_path, class: "btn btn-primary" %>
</div>
</div>
<table class="compare-table">
<thead>
<tr><th></th><th>Free</th><th>Premium Light</th><th>Premium Full</th></tr>
</thead>
<tbody>
<tr><td>Staff trasmissione / anno</td><td>1</td><td>5</td><td>Illimitato</td></tr>
<tr><td>Staff regia / anno</td><td>1</td><td>5</td><td>Illimitato</td></tr>
<tr><td>Partite in contemporanea</td><td>1</td><td>3</td><td>10</td></tr>
<tr><td>Live su Match Live TV</td><td>Sì</td><td>Sì</td><td>Sì</td></tr>
<tr><td>YouTube</td><td>No</td><td>Match TV Light</td><td>Canale società</td></tr>
<tr><td>Replay server</td><td>No</td><td>30 gg</td><td>90 gg</td></tr>
<tr><td>Download telefono</td><td>No</td><td>Sì</td><td>Sì</td></tr>
<tr><td>Prezzo</td><td>€0</td><td>€40/anno o €5/mese</td><td>€200/anno o €20/mese</td></tr>
</tbody>
</table>
<div class="card" style="margin-top:32px;text-align:center">
<h3 style="margin-top:0">Esigenze diverse?</h3>
<p style="color:#aaa;margin-bottom:16px">Per società con più squadre, tornei o integrazioni custom, contattaci: troviamo insieme l'offerta migliore.</p>
<%= mail_to "info@matchlivetv.eminux.it", "Contattaci", class: "btn btn-primary" %>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<% 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>
</div>

View File

@@ -0,0 +1,9 @@
<% 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>
</div>

View File

@@ -0,0 +1,20 @@
<% content_for :title, "Registrati — Match Live TV" %>
<section class="auth-page">
<h1>Registra la squadra</h1>
<p class="auth-lead">Crea l'account del referente (coach o dirigente), poi configurerai la squadra.</p>
<div class="card">
<%= form_with model: @user, url: public_signup_path do |f| %>
<%= f.label :name, "Nome" %>
<%= f.text_field :name, required: true %>
<%= f.label :email, "Email" %>
<%= f.email_field :email, required: true %>
<%= f.label :password, "Password" %>
<%= f.password_field :password, required: true, minlength: 8 %>
<%= f.label :password_confirmation, "Conferma password" %>
<%= f.password_field :password_confirmation, required: true %>
<%= f.submit "Continua", class: "btn btn-primary" %>
<% end %>
<p class="auth-footer">Hai già un account? <%= link_to "Accedi", public_login_path %></p>
</div>
</section>

View File

@@ -0,0 +1,37 @@
<% content_for :title, "Replay — #{@match.team.name} vs #{@match.opponent_name}" %>
<div class="wrap">
<%= link_to "← Tutte le dirette", public_live_index_path, class: "back-link" %>
<h1>Replay</h1>
<p style="color:#aaa"><%= @match.team.name %> vs <%= @match.opponent_name %></p>
<% if @recording %>
<div class="card" style="margin:16px 0">
<% if @session.hls_playback_url.present? %>
<video id="replay-player" class="replay-video" controls playsinline></video>
<script src="https://cdn.jsdelivr.net/npm/hls.js@1.5.7"></script>
<script>
(function() {
var src = "<%= j @session.hls_playback_url %>";
var video = document.getElementById("replay-player");
if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = src;
} else if (typeof Hls !== "undefined" && Hls.isSupported()) {
var hls = new Hls();
hls.loadSource(src);
hls.attachMedia(video);
} else {
video.outerHTML = "<p>Il replay non è disponibile su questo browser.</p>";
}
})();
</script>
<% else %>
<p>Registrazione indicizzata. Il playback dipende dalla retention del piano Premium.</p>
<% end %>
<% if @recording.expires_at %>
<p class="stream-ended-meta" style="margin-top:12px">Disponibile fino al <%= l @recording.expires_at, format: :long %></p>
<% end %>
</div>
<% end %>
</div>

View File

@@ -0,0 +1,15 @@
<% content_for :title, "Accedi — Match Live TV" %>
<section class="auth-page">
<h1>Accedi</h1>
<div class="card">
<%= form_with url: public_login_path do |f| %>
<%= label_tag :email, "Email" %>
<%= email_field_tag :email, params[:email], required: true %>
<%= label_tag :password, "Password" %>
<%= password_field_tag :password, required: true %>
<%= submit_tag "Entra", class: "btn btn-primary" %>
<% end %>
<p class="auth-footer">Nuovo? <%= link_to "Registrati", public_signup_path %></p>
</div>
</section>

View File

@@ -0,0 +1,49 @@
<% content_for :title, "Abbonamento — #{@team.name}" %>
<div class="wrap" style="padding-top:20px">
<h1>Abbonamento</h1>
<p>Squadra: <strong><%= @team.name %></strong> · Piano attuale: <strong><%= @entitlements.plan.name %></strong></p>
<div class="plans">
<% @plans.each do |plan| %>
<div class="plan-card <%= 'featured' if plan.slug == @entitlements.plan.slug %>">
<h3><%= plan.name %></h3>
<% if plan.slug == "free" %>
<div class="plan-price">€0</div>
<% elsif plan.slug == "premium_light" %>
<div class="plan-price" style="color:#e53935">€40/anno</div>
<p style="color:#888;font-size:0.85rem">oppure €5/mese</p>
<% else %>
<div class="plan-price">€200/anno</div>
<p style="color:#888;font-size:0.85rem">oppure €20/mese</p>
<% end %>
<ul>
<li>Staff trasmissione: <%= plan.max_staff_transmission || "illimitato" %> / anno</li>
<li>Staff regia: <%= plan.max_staff_regia || "illimitato" %> / anno</li>
<li>Partite: <%= plan.concurrent_streams_limit || "illimitate" %> in contemporanea</li>
<li>Replay: <%= plan.recordings_enabled? ? "#{plan.recording_days} giorni" : "no" %></li>
<li>YouTube: <% if plan.slug == "premium_light" %>Match TV Light<% elsif plan.youtube_enabled? %>canale società<% else %>no<% end %></li>
</ul>
<% if plan.slug == @entitlements.plan.slug %>
<span class="btn btn-secondary" style="opacity:0.7">Piano attuale</span>
<% elsif plan.slug == "free" %>
<p style="color:#888;font-size:0.85rem">Contatta il supporto per downgrade.</p>
<% elsif MatchLiveTv.stripe_enabled? %>
<%= link_to "Attiva #{plan.name}", public_team_checkout_path(@team, plan: plan.slug), class: "btn btn-primary" %>
<% else %>
<p style="color:#888">Stripe non configurato</p>
<% end %>
</div>
<% end %>
</div>
<div class="card" style="margin-top:24px;text-align:center">
<p style="color:#aaa">Esigenze diverse? <%= mail_to "info@matchlivetv.eminux.it", "Contattaci" %> per un'offerta su misura.</p>
</div>
<% if @entitlements.premium_active? && MatchLiveTv.stripe_enabled? %>
<p style="margin-top:20px"><%= button_to "Gestisci su Stripe", public_team_portal_path(@team), class: "btn btn-secondary" %></p>
<% end %>
<p><%= link_to "← Dashboard", public_team_dashboard_path(@team) %></p>
</div>

View File

@@ -0,0 +1,82 @@
<% content_for :title, "#{@team.name} — Dashboard" %>
<div class="wrap" style="padding-top:20px">
<h1><%= @team.name %></h1>
<p>
Piano: <strong><%= @entitlements.plan.name %></strong>
· Staff trasmissione: <strong><%= @entitlements.staff_count_for("transmission") %> / <%= @entitlements.max_staff_transmission || "∞" %></strong>
· Staff regia: <strong><%= @entitlements.staff_count_for("regia") %> / <%= @entitlements.max_staff_regia || "∞" %></strong>
· Partite attive: <strong><%= @entitlements.concurrent_streams_used %><% if @entitlements.concurrent_streams_limit %> / <%= @entitlements.concurrent_streams_limit %><% else %> (illimitate)<% end %></strong>
</p>
<div style="display:flex;gap:12px;flex-wrap:wrap;margin:20px 0">
<%= link_to "Abbonamento", public_team_billing_path(@team), class: "btn btn-primary" %>
<%= link_to "Aggiungi staff", public_team_invite_path(@team), class: "btn btn-secondary" %>
<%= link_to "Dirette live", public_live_index_path, class: "btn btn-secondary" %>
</div>
<h2>Staff attivo</h2>
<div class="card">
<% if @members.any? %>
<table class="data">
<thead><tr><th>Nome</th><th>Email</th><th>Ruolo</th><th></th></tr></thead>
<tbody>
<% @members.each do |ut| %>
<tr>
<td><%= ut.user.name %></td>
<td><%= ut.user.email %></td>
<td><%= ut.staff_kind == "regia" ? "Regia" : "Trasmissione" %></td>
<td>
<%= button_to "Revoca", public_team_remove_member_path(@team, ut.user), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem", form: { data: { turbo_confirm: "Revocare l'accesso a #{ut.user.name}?" } } %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p style="color:#888">Nessun membro staff. <%= link_to "Aggiungi qualcuno", public_team_invite_path(@team) %>.</p>
<% end %>
</div>
<h2>Staff in attesa</h2>
<div class="card">
<% if @pending_invitations.any? %>
<table class="data">
<thead><tr><th>Email</th><th>Ruolo</th><th>Scade</th><th></th></tr></thead>
<tbody>
<% @pending_invitations.each do |inv| %>
<tr>
<td><%= inv.email %></td>
<td><%= inv.staff_kind == "regia" ? "Regia" : "Trasmissione" %></td>
<td><%= l inv.expires_at, format: :short %></td>
<td>
<%= button_to "Annulla", public_team_destroy_invitation_path(@team, inv), method: :delete, class: "btn btn-secondary", style: "padding:4px 10px;font-size:0.8rem" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p style="color:#888">Nessuna richiesta in sospeso.</p>
<% end %>
</div>
<% if @entitlements.can_access_recordings? && @recordings.any? %>
<h2>Archivio gare</h2>
<div class="card">
<table class="data">
<thead><tr><th>Partita</th><th>Replay</th></tr></thead>
<tbody>
<% @recordings.each do |rec| %>
<tr>
<td><%= rec.stream_session.match.team.name %> vs <%= rec.stream_session.match.opponent_name %></td>
<td><%= link_to "Guarda", rec.replay_url %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<p style="color:#888;margin-top:24px">Usa l'app mobile con le stesse credenziali per avviare partite e dirette.</p>
</div>

View File

@@ -0,0 +1,28 @@
<% content_for :title, "Aggiungi staff — #{@team.name}" %>
<div class="wrap" style="padding-top:20px">
<h1>Aggiungi staff</h1>
<p>
Trasmissione: <strong><%= @entitlements.staff_count_for("transmission") %> / <%= @entitlements.max_staff_transmission || "∞" %></strong>
· Regia: <strong><%= @entitlements.staff_count_for("regia") %> / <%= @entitlements.max_staff_regia || "∞" %></strong>
</p>
<div class="card" style="max-width:520px">
<%= form_with url: public_team_invite_path(@team), method: :post do %>
<%= label_tag :staff_kind, "Ruolo" %>
<%= select_tag :staff_kind, options_for_select([
["Trasmissione (camera / streaming)", "transmission"],
["Regia (punteggio)", "regia"]
], params[:staff_kind] || "transmission") %>
<%= label_tag :email, "Email del membro staff" %>
<%= email_field_tag :email, params[:email], required: true %>
<%= submit_tag "Genera link di accesso", class: "btn btn-primary" %>
<% end %>
<% if defined?(@invite_url) && @invite_url.present? %>
<p style="margin-top:16px">Condividi (valido 7 giorni):</p>
<code style="word-break:break-all;display:block;background:#0a0a0e;padding:12px;border-radius:8px"><%= @invite_url %></code>
<p style="color:#888;font-size:0.88rem;margin-top:8px">Il membro dello staff si registra con la stessa email e accetta l'accesso.</p>
<% end %>
</div>
<p><%= link_to "← Dashboard", public_team_dashboard_path(@team) %></p>
</div>

View File

@@ -0,0 +1,21 @@
<% content_for :title, "Crea squadra — Match Live TV" %>
<section class="auth-page">
<h1>La tua squadra</h1>
<p class="auth-lead">Ultimo passo: nome, sport e piano iniziale.</p>
<div class="card card-wide">
<%= form_with url: public_teams_path do %>
<%= label_tag "team[name]", "Nome squadra" %>
<%= text_field_tag "team[name]", nil, required: true %>
<%= label_tag "team[sport]", "Sport" %>
<%= select_tag "team[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], "volleyball") %>
<%= label_tag :plan, "Piano iniziale" %>
<%= select_tag :plan, options_for_select([
["Free — 1 staff trasmissione + 1 regia, 1 live", "free"],
["Premium Light — €40/anno o €5/mese", "premium_light"],
["Premium Full — €200/anno o €20/mese", "premium_full"]
], "free") %>
<%= submit_tag "Crea squadra", class: "btn btn-primary" %>
<% end %>
</div>
</section>

View File

@@ -0,0 +1,13 @@
<footer class="site-footer">
<div class="wrap">
<div>
<strong style="color:#fff">Match Live TV</strong> — Ogni partita, ogni evento, per chi non può esserci
</div>
<div>
<%= link_to "Prezzi", public_prezzi_path %> ·
<%= link_to "Dirette", public_live_index_path %> ·
<%= link_to "Privacy", public_privacy_path %> ·
<%= link_to "Termini", public_termini_path %>
</div>
</div>
</footer>

View File

@@ -0,0 +1,76 @@
<% live_section = request.path.start_with?("/live") %>
<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 %>
<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>
<span class="nav-toggle-bar" aria-hidden="true"></span>
<span class="nav-toggle-bar" aria-hidden="true"></span>
</button>
</div>
</div>
<nav id="site-nav" class="nav" aria-label="Menu principale" aria-hidden="true">
<div class="nav-panel">
<%= 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 "Dirette live", public_live_index_path, class: (live_section ? "nav-active" : nil) %>
<div class="nav-actions">
<% if logged_in? %>
<% if current_user.teams.any? %>
<%= link_to "Dashboard", public_team_dashboard_path(current_user.teams.first), class: "nav-link-item" %>
<% end %>
<%= button_to "Esci", public_logout_path, method: :delete, class: "btn btn-secondary nav-btn" %>
<% else %>
<%= link_to "Accedi", public_login_path, class: "nav-link-item" %>
<%= link_to "Registra squadra", public_signup_path, class: "btn btn-primary nav-btn" %>
<% end %>
</div>
</div>
</nav>
</div>
<div class="nav-backdrop" id="nav-backdrop" aria-hidden="true"></div>
<script>
(function () {
var chrome = document.querySelector(".site-chrome");
var toggle = document.querySelector(".nav-toggle");
var backdrop = document.getElementById("nav-backdrop");
var nav = document.getElementById("site-nav");
if (!chrome || !toggle || !nav) return;
function setOpen(open) {
chrome.classList.toggle("nav-open", open);
document.body.classList.toggle("nav-menu-open", open);
toggle.setAttribute("aria-expanded", open ? "true" : "false");
toggle.setAttribute("aria-label", open ? "Chiudi menu" : "Apri menu");
nav.setAttribute("aria-hidden", open ? "false" : "true");
if (backdrop) backdrop.setAttribute("aria-hidden", open ? "false" : "true");
}
function closeMenu() { setOpen(false); }
toggle.addEventListener("click", function (e) {
e.stopPropagation();
document.body.classList.contains("nav-menu-open") ? closeMenu() : setOpen(true);
});
if (backdrop) backdrop.addEventListener("click", closeMenu);
nav.querySelectorAll("a, button").forEach(function (el) {
el.addEventListener("click", closeMenu);
});
window.addEventListener("resize", function () {
if (window.matchMedia("(min-width: 900px)").matches) closeMenu();
});
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeMenu();
});
})();
</script>

View File

@@ -0,0 +1,7 @@
<% 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." %>
<meta name="description" content="<%= description %>">
<meta property="og:title" content="<%= content_for?(:title) ? yield(:title) : 'Match Live TV' %>">
<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">