diff --git a/README.md b/README.md
index 74f0530..3a523a3 100644
--- a/README.md
+++ b/README.md
@@ -14,12 +14,14 @@ Applicazione web **Progressive Web App (PWA)** per tracciare i punteggi di parti
- Tracciamento punti in tempo reale per entrambe le squadre
- Conteggio automatico dei set (modalità 2/3 o 3/5)
- Indicatore visivo del servizio
+ - Blocco incremento punti a set concluso
- Cronologia punti con striscia visiva
- **Formazioni Squadra**
- Visualizzazione interattiva dei 6 giocatori in campo
- Rotazione automatica regolamentare al cambio palla
- Configurazione manuale dei numeri di maglia
+ - Dialog cambi con uno o due cambi (IN → OUT) e validazioni
- Supporto logica pallavolo ufficiale (25 punti + 2 di vantaggio, tie-break a 15 nel set decisivo)
- **Controlli Multimodali**
@@ -152,6 +154,7 @@ Serve i file dalla cartella `/dist` per testare la build prima del deploy.
| `Ctrl + ↑` | Incrementa punti |
| `Ctrl + ↓` | Decrementa punti |
| `Ctrl + →` | Incrementa set |
+| `Ctrl + C` | Apri dialog cambi |
### Controlli Tastiera Squadra Guest
@@ -160,6 +163,7 @@ Serve i file dalla cartella `/dist` per testare la build prima del deploy.
| `Shift + ↑` | Incrementa punti |
| `Shift + ↓` | Decrementa punti |
| `Shift + →` | Incrementa set |
+| `Shift + C` | Apri dialog cambi |
### Comandi Globali
diff --git a/src/components/HomePage/HomePage.html b/src/components/HomePage/HomePage.html
index 06e7993..718083e 100644
--- a/src/components/HomePage/HomePage.html
+++ b/src/components/HomePage/HomePage.html
@@ -68,6 +68,35 @@
Ok
+
+ Scegli squadra
+
+ {{ sp.nomi.home }}
+ {{ sp.nomi.guest }}
+
+
+
+
+
{{ sp.nomi[diaCambi.team] }}: CAMBIO
+
+
+
+ →
+
+
+
+
+ →
+
+
+
+
+
+
+ CONFERMA
+
+
+
@@ -193,6 +222,9 @@
PUNTEGGIO
FORMAZIONI
+
+ CAMBI
+
STRISCIA
diff --git a/src/components/HomePage/HomePage.js b/src/components/HomePage/HomePage.js
index e426b20..9c27938 100644
--- a/src/components/HomePage/HomePage.js
+++ b/src/components/HomePage/HomePage.js
@@ -11,6 +11,15 @@ export default {
home: "",
guest: "",
},
+ diaCambi: {
+ show: false,
+ team: "home",
+ guest: { cambi: [{ in: "", out: "" }, { in: "", out: "" }] },
+ home: { cambi: [{ in: "", out: "" }, { in: "", out: "" }] },
+ },
+ diaCambiTeam: {
+ show: false,
+ },
visuForm: false,
visuButt: true,
visuStriscia: true,
@@ -42,6 +51,28 @@ export default {
computed: {
isPunteggioZeroZero() {
return this.sp.punt.home === 0 && this.sp.punt.guest === 0;
+ },
+ cambiConfermabili() {
+ const team = this.diaCambi.team;
+ const cambi = this.diaCambi[team].cambi || [];
+ let hasComplete = false;
+ let allValid = true;
+
+ cambi.forEach((cambio) => {
+ const teamIn = (cambio.in || "").trim();
+ const teamOut = (cambio.out || "").trim();
+
+ if (!teamIn && !teamOut) {
+ return;
+ }
+ if (!teamIn || !teamOut) {
+ allValid = false;
+ return;
+ }
+ hasComplete = true;
+ });
+
+ return allValid && hasComplete;
}
},
methods: {
@@ -214,6 +245,79 @@ export default {
this.disabilitaTastiSpeciali();
this.diaNomi.show = true;
},
+ resettaCambi(team) {
+ const teams = team ? [team] : ["home", "guest"];
+ teams.forEach((t) => {
+ this.diaCambi[t].cambi.forEach((cambio) => {
+ cambio.in = "";
+ cambio.out = "";
+ });
+ });
+ },
+ apriDialogCambi() {
+ this.disabilitaTastiSpeciali();
+ this.diaCambiTeam.show = true;
+ },
+ apriDialogCambiTeam(team) {
+ this.disabilitaTastiSpeciali();
+ this.diaCambi.team = team;
+ this.resettaCambi(team);
+ this.diaCambi.show = true;
+ },
+ selezionaTeamCambi(team) {
+ this.diaCambiTeam.show = false;
+ this.apriDialogCambiTeam(team);
+ },
+ chiudiDialogCambi() {
+ this.diaCambi.show = false;
+ this.resettaCambi(this.diaCambi.team);
+ this.abilitaTastiSpeciali();
+ },
+ confermaCambi() {
+ if (!this.cambiConfermabili) {
+ return;
+ }
+
+ const team = this.diaCambi.team;
+ const cambi = (this.diaCambi[team].cambi || [])
+ .map((cambio) => ({
+ team,
+ in: (cambio.in || "").trim(),
+ out: (cambio.out || "").trim(),
+ }))
+ .filter((cambio) => cambio.in || cambio.out);
+
+ const form = this.sp.form[team].map((val) => String(val).trim());
+ const formAggiornata = [...form];
+
+ for (const cambio of cambi) {
+ if (!/^\d+$/.test(cambio.in) || !/^\d+$/.test(cambio.out)) {
+ this.$waveui.notify("Inserisci solo numeri nei campi", "warning");
+ return;
+ }
+ if (cambio.in === cambio.out) {
+ this.$waveui.notify(`Numero IN e OUT uguali per ${cambio.team}`, "warning");
+ return;
+ }
+ if (formAggiornata.includes(cambio.in)) {
+ this.$waveui.notify(`Numero ${cambio.in} già presente in formazione ${cambio.team}`, "warning");
+ return;
+ }
+ if (!formAggiornata.includes(cambio.out)) {
+ this.$waveui.notify(`Numero ${cambio.out} non presente in formazione ${cambio.team}`, "warning");
+ return;
+ }
+
+ const idx = formAggiornata.findIndex((val) => String(val).trim() === cambio.out);
+ if (idx !== -1) {
+ formAggiornata.splice(idx, 1, cambio.in);
+ }
+ }
+
+ this.sp.form[team] = formAggiornata;
+
+ this.chiudiDialogCambi();
+ },
disabilitaTastiSpeciali() {
window.removeEventListener("keydown", this.funzioneTastiSpeciali);
},
@@ -221,32 +325,82 @@ export default {
window.addEventListener("keydown", this.funzioneTastiSpeciali);
},
funzioneTastiSpeciali(e) {
- e.preventDefault();
+ if (this.diaNomi.show || this.diaCambi.show || this.diaCambiTeam.show) {
+ return;
+ }
+
+ const target = e.target;
+ const path = typeof e.composedPath === "function" ? e.composedPath() : [];
+ const elements = [target, ...path].filter(Boolean);
+ const isTypingField = elements.some((el) => {
+ if (!el || !el.tagName) {
+ return false;
+ }
+ const tag = String(el.tagName).toLowerCase();
+ if (tag === "input" || tag === "textarea") {
+ return true;
+ }
+ if (el.isContentEditable) {
+ return true;
+ }
+ if (el.classList && (el.classList.contains("w-input") || el.classList.contains("w-textarea"))) {
+ return true;
+ }
+ const contentEditable = el.getAttribute && el.getAttribute("contenteditable");
+ return contentEditable === "true";
+ });
+ if (isTypingField) {
+ return;
+ }
+
+ let handled = false;
if (e.ctrlKey && e.key == "m") {
this.diaNomi.show = true
+ handled = true;
} else if (e.ctrlKey && e.key == "b") {
this.visuButt = !this.visuButt
+ handled = true;
} else if (e.ctrlKey && e.key == "f") {
document.documentElement.requestFullscreen();
+ handled = true;
} else if (e.ctrlKey && e.key == "s") {
this.speak();
+ handled = true;
} else if (e.ctrlKey && e.key == "z") {
this.visuForm = !this.visuForm
+ handled = true;
} else if (e.ctrlKey && e.key == "ArrowUp") {
this.incPunt("home")
+ handled = true;
} else if (e.ctrlKey && e.key == "ArrowDown") {
this.decPunt("home")
+ handled = true;
} else if (e.ctrlKey && e.key == "ArrowRight") {
this.incSet("home")
+ handled = true;
} else if (e.shiftKey && e.key == "ArrowUp") {
this.incPunt("guest")
+ handled = true;
} else if (e.shiftKey && e.key == "ArrowDown") {
this.decPunt("guest")
+ handled = true;
} else if (e.shiftKey && e.key == "ArrowRight") {
this.incSet("guest")
+ handled = true;
} else if (e.ctrlKey && e.key == "ArrowLeft") {
this.cambiaPalla()
+ handled = true;
+ } else if (e.ctrlKey && (e.key == "c" || e.key == "C")) {
+ this.apriDialogCambiTeam("home")
+ handled = true;
+ } else if (e.shiftKey && (e.key == "c" || e.key == "C")) {
+ this.apriDialogCambiTeam("guest")
+ handled = true;
} else { return false }
+
+ if (handled) {
+ e.preventDefault();
+ }
}
}
}
diff --git a/src/style.css b/src/style.css
index b061700..f00b946 100644
--- a/src/style.css
+++ b/src/style.css
@@ -190,3 +190,75 @@ button:focus-visible {
height: 0;
margin: 0;
}
+
+.cambi-rows {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 8px 0;
+}
+
+.cambi-dialog {
+ padding: 8px 6px 2px;
+}
+
+.cambi-title {
+ text-align: center;
+ font-weight: 800;
+ letter-spacing: 0.5px;
+ margin-bottom: 8px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.15);
+}
+
+.cambi-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+}
+
+.cambi-arrow {
+ font-weight: 700;
+ font-size: 18px;
+ line-height: 1;
+ padding: 6px 8px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.08);
+ white-space: nowrap;
+}
+
+.cambi-input {
+ min-width: 48px;
+ max-width: 64px;
+}
+
+.cambi-input input,
+.cambi-input .w-input__input,
+.cambi-input .w-input__field {
+ border: 2px solid rgba(255, 255, 255, 0.35);
+ border-radius: 8px;
+ padding: 6px 10px;
+ color: #000;
+ text-align: center;
+ box-sizing: border-box;
+}
+
+.cambi-in input,
+.cambi-in .w-input__input,
+.cambi-in .w-input__field {
+ background: rgba(120, 200, 120, 0.4);
+}
+
+.cambi-out input,
+.cambi-out .w-input__input,
+.cambi-out .w-input__field {
+ background: rgba(200, 120, 120, 0.4);
+}
+
+.cambi-input input:focus,
+.cambi-input .w-input__input:focus,
+.cambi-input .w-input__field:focus {
+ border-color: rgba(255, 255, 255, 0.7);
+ outline: none;
+}