Files
MatchLiveTv/backend/public/regia.js
Emiliano Frascaro f4b7be0f80 Billing Stripe, link regia mobile e staff solo trasmissione.
Aggiunge fatturazione club, pagina regia condivisibile senza account, roster squadre e rimuove la modalità controller dall'app (v1.1.0).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 07:23:13 +02:00

253 lines
7.8 KiB
JavaScript

(function () {
const root = document.getElementById("regia-app");
if (!root) return;
const cfg = {
token: root.dataset.token,
statusUrl: root.dataset.statusUrl,
scoreUrl: root.dataset.scoreUrl,
pauseUrl: root.dataset.pauseUrl,
stopUrl: root.dataset.stopUrl,
cableUrl: root.dataset.cableUrl,
hlsUrl: root.dataset.hlsUrl,
homeName: root.dataset.homeName,
awayName: root.dataset.awayName,
shareUrl: root.dataset.shareUrl,
shareTitle: root.dataset.shareTitle,
streamClosed: root.dataset.streamClosed === "true"
};
const els = {
badge: document.getElementById("regia-badge"),
setsLine: document.getElementById("sets-line"),
scoreLine: document.getElementById("score-line"),
partialsLine: document.getElementById("partials-line"),
homePoints: document.getElementById("home-points"),
toast: document.getElementById("regia-toast"),
modal: document.getElementById("regia-modal"),
modalTitle: document.getElementById("modal-title"),
modalBody: document.getElementById("modal-body"),
modalConfirm: document.getElementById("modal-confirm"),
modalCancel: document.getElementById("modal-cancel"),
preview: document.getElementById("regia-preview"),
previewPlaceholder: document.getElementById("regia-preview-placeholder")
};
let pendingAction = null;
let hls = null;
function toast(msg) {
els.toast.textContent = msg;
els.toast.classList.add("visible");
setTimeout(() => els.toast.classList.remove("visible"), 2800);
}
function formatPartials(partials) {
if (!partials || !partials.length) return "";
return partials
.map((p) => `Set ${p.set || p["set"]} ${p.home || p["home"]}-${p.away || p["away"]}`)
.join(" · ");
}
function applyScorePayload(data) {
const s = data.score;
if (!s) return;
const homePts = s.home_points ?? s.homePoints;
const awayPts = s.away_points ?? s.awayPoints;
const homeSets = s.home_sets ?? s.homeSets;
const awaySets = s.away_sets ?? s.awaySets;
const currentSet = s.current_set ?? s.currentSet;
const awayEl = document.getElementById("away-points");
els.homePoints.textContent = homePts;
if (awayEl) awayEl.textContent = awayPts;
els.setsLine.textContent = `Set ${currentSet} · Set vinti ${homeSets}-${awaySets}`;
els.scoreLine.textContent = `${cfg.homeName} ${homePts} - ${awayPts} ${cfg.awayName}`;
const partialsText = formatPartials(s.set_partials || s.setPartials);
if (partialsText) {
els.partialsLine.textContent = `Parziali: ${partialsText}`;
els.partialsLine.hidden = false;
}
}
function setBadge(data) {
if (data.stream_closed) {
els.badge.textContent = "Terminata";
els.badge.className = "regia-badge regia-badge--ended";
return;
}
if (data.on_air) {
els.badge.textContent = "In onda";
els.badge.className = "regia-badge regia-badge--live";
} else {
els.badge.textContent = "In attesa";
els.badge.className = "regia-badge regia-badge--wait";
}
}
async function postScore(action) {
const res = await fetch(cfg.scoreUrl, {
method: "PATCH",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ cmd: action })
});
if (!res.ok) throw new Error("Errore aggiornamento punteggio");
return res.json();
}
async function handleAction(action) {
try {
const data = await postScore(action);
applyScorePayload(data);
setBadge(data);
if (data.set_won && action !== "close_set") {
const winner = data.winner === "home" ? cfg.homeName : cfg.awayName;
openModal("Set vinto", `${winner} ha vinto il set. Chiudere il set?`, () => postScore("close_set"));
} else if (data.match_won) {
const winner = data.winner === "home" ? cfg.homeName : cfg.awayName;
openModal("Partita vinta", `${winner} ha vinto la partita. Chiudere la diretta?`, () => stopStream());
}
} catch (e) {
toast(e.message || "Errore");
}
}
function openModal(title, body, onConfirm) {
els.modalTitle.textContent = title;
els.modalBody.textContent = body;
els.modal.classList.add("open");
pendingAction = onConfirm;
}
function closeModal() {
els.modal.classList.remove("open");
pendingAction = null;
}
els.modalConfirm.addEventListener("click", async () => {
const fn = pendingAction;
closeModal();
if (fn) {
try {
const data = await fn();
if (data) {
applyScorePayload(data);
setBadge(data);
}
} catch (e) {
toast(e.message || "Errore");
}
}
});
els.modalCancel.addEventListener("click", closeModal);
root.querySelectorAll("[data-action]").forEach((btn) => {
btn.addEventListener("click", () => {
const action = btn.dataset.action;
if (action === "close_set") {
openModal("Chiudi set", "Confermi la chiusura del set corrente?", () => postScore("close_set"));
} else {
handleAction(action);
}
});
});
async function pollStatus() {
try {
const res = await fetch(cfg.statusUrl, { headers: { Accept: "application/json" } });
if (!res.ok) return;
const data = await res.json();
applyScorePayload(data);
setBadge(data);
if (data.stream_closed) {
cfg.streamClosed = true;
stopPreview();
}
} catch (_) {}
}
function initPreview() {
if (cfg.streamClosed || !els.preview || !cfg.hlsUrl) return;
if (els.preview.canPlayType("application/vnd.apple.mpegurl")) {
els.preview.src = cfg.hlsUrl;
} else if (window.Hls && Hls.isSupported()) {
hls = new Hls({ maxBufferLength: 8 });
hls.loadSource(cfg.hlsUrl);
hls.attachMedia(els.preview);
}
els.preview.addEventListener("playing", () => {
if (els.previewPlaceholder) els.previewPlaceholder.hidden = true;
});
}
function stopPreview() {
if (hls) {
hls.destroy();
hls = null;
}
if (els.preview) els.preview.remove();
if (els.previewPlaceholder) {
els.previewPlaceholder.hidden = false;
els.previewPlaceholder.textContent = "Diretta terminata";
}
}
async function pauseStream() {
await fetch(cfg.pauseUrl, { method: "POST", headers: { Accept: "application/json" } });
toast("Trasmissione in pausa");
}
async function stopStream() {
const ok = confirm("Chiudere definitivamente la diretta?");
if (!ok) return null;
const res = await fetch(cfg.stopUrl, { method: "POST", headers: { Accept: "application/json" } });
if (!res.ok) throw new Error("Errore chiusura");
const data = await res.json();
cfg.streamClosed = true;
stopPreview();
toast("Diretta chiusa");
return data;
}
document.getElementById("btn-pause")?.addEventListener("click", () => pauseStream().catch((e) => toast(e.message)));
document.getElementById("btn-stop")?.addEventListener("click", () => stopStream().catch((e) => toast(e.message)));
async function shareLink() {
const payload = {
title: cfg.shareTitle,
text: "Apri questo link per gestire il punteggio della diretta:",
url: cfg.shareUrl
};
if (navigator.share) {
try {
await navigator.share(payload);
return;
} catch (err) {
if (err.name === "AbortError") return;
}
}
await copyLink();
}
async function copyLink() {
try {
await navigator.clipboard.writeText(cfg.shareUrl);
toast("Link copiato");
} catch (_) {
prompt("Copia il link:", cfg.shareUrl);
}
}
document.getElementById("btn-share")?.addEventListener("click", () => shareLink());
document.getElementById("btn-copy")?.addEventListener("click", () => copyLink());
initPreview();
setInterval(pollStatus, 4000);
pollStatus();
})();