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>
This commit is contained in:
252
backend/public/regia.js
Normal file
252
backend/public/regia.js
Normal file
@@ -0,0 +1,252 @@
|
||||
(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();
|
||||
})();
|
||||
Reference in New Issue
Block a user