6 Commits

Author SHA1 Message Date
2c6416bfe0 Aggiorna README.md 2026-01-28 13:31:42 +01:00
9a808e566d Merge pull request 'wip-formazione' (#3) from wip-formazione into master
Reviewed-on: #3
2026-01-28 12:00:45 +01:00
6c6ac7fc29 Limita il cambio palla solo a inizio set (0-0)
- Aggiunge computed property isPunteggioZeroZero per verificare lo stato del punteggio
- Crea metodo cambiaPalla() con validazione che blocca il cambio se il punteggio non è 0-0
- Disabilita il pulsante cambio palla quando il punteggio non è 0-0
- Mostra notifica di avviso se si tenta il cambio palla durante il set
- Aggiorna scorciatoia tastiera Ctrl+ArrowLeft per usare la stessa validazione
2026-01-26 13:45:22 +01:00
bbe0862241 Implementa rotazione regolamentare con cambio palla
- La formazione ruota solo quando si conquista il servizio (cambio palla)
- Aggiunge array servizioPrecedente per tracciare i cambi palla
- Fix decPunt per annullare correttamente la rotazione
- Fix resetta per pulire lo stack dei servizi precedenti
2026-01-25 17:57:22 +01:00
26d647dce7 Aggiunge configurazione manuale numeri di maglia
- Aggiunge campi input nel dialog configurazione per modificare manualmente i numeri di maglia dei giocatori
- Disegna campi da pallavolo stilizzati (220x220px) con linea dei 3 metri posizionata a 1/3 dall'alto
- Layout corrisponde alla visualizzazione sul campo (ordine rotazione [3,2,1,4,5,0])
- Proporzioni realistiche: zona anteriore 33%, zona posteriore 67%
- Sfondo marrone chiaro e bordi grigi per migliore leggibilità
2026-01-25 17:46:31 +01:00
a72bc1844e Blocca assegnazione punti al raggiungimento della vittoria
Aggiunge controllo che impedisce di assegnare ulteriori punti quando
viene raggiunta la condizione di vittoria (25 punti con 2 di vantaggio
nei set 1-4, 15 punti con 2 di vantaggio nel set decisivo)
2026-01-24 19:00:07 +01:00
4 changed files with 397 additions and 16 deletions

251
README.md
View File

@@ -1,8 +1,249 @@
# Vue 3 + Vite # Segnapunti Anto
# nvm use v20.2.0
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more. Applicazione web **Progressive Web App (PWA)** per tracciare i punteggi di partite di pallavolo in tempo reale.
## Recommended IDE Setup ---
## Panoramica
**Segnapunti Anto** è un'applicazione digitale per il tracciamento dei punteggi durante partite di pallavolo, ottimizzata per l'uso su tablet e smartphone. Sviluppata per il team Antoniana, l'app fornisce un'interfaccia fullscreen touch-friendly con supporto offline e controlli da tastiera.
### Funzionalità Principali
- **Gestione Completa Partite**
- Tracciamento punti in tempo reale per entrambe le squadre
- Conteggio automatico dei set (fino a 5 set)
- Indicatore visivo del servizio
- 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
- Supporto logica pallavolo ufficiale (25 punti + 2 di vantaggio, 5° set a 15)
- **Controlli Multimodali**
- Scorciatoie da tastiera complete (vedi sezione [Shortcuts](#shortcuts))
- Sintesi vocale per annunci punteggio in italiano (Web Speech API)
- **Personalizzazione**
- Configurazione dinamica nomi squadre
- Toggle layout orizzontale (inverti home/guest)
- Modalità visualizzazione: punteggio semplice o formazioni complete
- Nascondi/mostra controlli e cronologia
---
## Requisiti
### Requisiti di Sistema
#### Per Sviluppo
- **Sistema Operativo**: Linux, macOS, Windows (WSL2 consigliato)
- **Node.js**: v20.2.0 o superiore (LTS consigliato)
- **npm**: v9.0.0 o superiore (incluso con Node.js)
- **RAM**: Minimo 2GB, consigliato 4GB
- **Spazio Disco**: ~500MB per dipendenze e build
#### Per Deployment
- **Server Web**: Qualsiasi server statico (nginx, Apache, Vercel, Netlify)
- **HTTPS**: Obbligatorio per Service Worker e PWA (eccetto localhost)
- **Connessione Internet**: Solo per primo caricamento (poi funziona offline)
### Requisiti Browser (Utente Finale)
| Requisito | Dettaglio | Necessità |
|-----------|-----------|-----------|
| **JavaScript ES6+** | Supporto moduli, arrow functions, async/await | Obbligatorio |
| **Service Worker API** | Per funzionalità offline PWA | Obbligatorio |
| **Fullscreen API** | Per modalità schermo intero | Consigliato |
| **Web Speech API** | Per sintesi vocale punteggi | Opzionale |
| **Local Storage** | Per persistenza configurazioni | Consigliato |
### Browser Testati e Supportati
| Browser | Versione Minima | Supporto | Note |
|---------|-----------------|----------|------|
| Chrome/Chromium | 90+ | ✅ Completo | Consigliato per tutte le features |
| Firefox | 88+ | ✅ Completo | Supporto completo PWA e Speech API |
---
## Installazione e Setup
### Prerequisiti
- **Node.js** v20.2.0 (consigliato)
- **npm** o **yarn**
### Installazione con NVM (consigliato)
```bash
# Installa la versione corretta di Node.js
nvm install v20.2.0
nvm use v20.2.0
# Clona il repository
git clone <repository-url>
cd segnapunti
# Installa le dipendenze
npm install
```
---
## Comandi per Sviluppo
### Dev Server
Avvia il server di sviluppo con hot-reload:
```bash
npm run dev
```
L'applicazione sarà disponibile su [http://localhost:5173](http://localhost:5173)
### Modalità Sviluppo
- Hot Module Replacement (HMR) attivo
- Source maps per debugging
- Vue DevTools supportato
- Errori e warnings in console
---
## Comandi per Build
### Build Produzione
Genera i file ottimizzati per il deployment:
```bash
npm run build
```
**Output:**
- Cartella `/dist` con file statici ottimizzati
- Service Worker generato automaticamente
- PWA manifest configurato
- Assets minificati e con hash per cache busting
- Base path: `/segnap` (modificabile in `vite.config.js`)
### Preview Build
Anteprima locale della build di produzione:
```bash
npm run preview
```
Serve i file dalla cartella `/dist` per testare la build prima del deploy.
---
## Shortcuts
### Controlli Tastiera Squadra Home
| Scorciatoia | Azione |
|-------------|--------|
| `Ctrl + ↑` | Incrementa punti |
| `Ctrl + ↓` | Decrementa punti |
| `Ctrl + →` | Incrementa set |
### Controlli Tastiera Squadra Guest
| Scorciatoia | Azione |
|-------------|--------|
| `Shift + ↑` | Incrementa punti |
| `Shift + ↓` | Decrementa punti |
| `Shift + →` | Incrementa set |
### Comandi Globali
| Scorciatoia | Azione |
|-------------|--------|
| `Ctrl + ←` | Cambio palla (servizio) - **solo a 0-0** |
| `Ctrl + M` | Apri configurazione nomi squadre e formazioni |
| `Ctrl + B` | Toggle visibilità barra pulsanti |
| `Ctrl + F` | Attiva/disattiva fullscreen |
| `Ctrl + S` | Annuncio vocale punteggio corrente |
| `Ctrl + Z` | Switch tra visualizzazione formazioni e punteggio |
---
## Configurazione PWA
L'applicazione è configurata come **Progressive Web App** nel file [vite.config.js](vite.config.js):
```javascript
VitePWA({
registerType: 'autoUpdate',
manifest: {
name: "app_segnap",
short_name: "segnap",
description: "Segnapunti standalone.",
background_color: "#eee",
theme_color: '#ffffff',
display: "fullscreen",
orientation: "landscape",
icons: [
{ src: 'segnap-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: 'segnap-512x512.png', sizes: '512x512', type: 'image/png' }
]
}
})
```
### Caratteristiche PWA
- **Display**: Fullscreen per massimizzare lo spazio visivo
- **Orientamento**: Landscape (orizzontale) ottimizzato per tablet
- **Auto-update**: Service Worker con aggiornamento automatico
- **Offline**: Funzionamento completo senza connessione internet
- **Installabile**: Aggiungibile alla home screen come app nativa
### Installazione PWA
**Android/Desktop (Chrome):**
- Menu → "Installa app" o icona (⊕) nella barra degli indirizzi
**iOS (Safari):**
- Share (□↑) → "Aggiungi a Home"
---
## Logica Regolamentare Pallavolo
### Vittoria Set
- **Set regolari (1-4)**: Primo a 25 punti con almeno 2 di vantaggio
- **Set decisivo (5°)**: Primo a 15 punti con almeno 2 di vantaggio
- **Blocco automatico**: Non consente assegnare punti oltre la vittoria
### Rotazione Formazione
La rotazione avviene **automaticamente** quando:
1. La squadra **conquista il servizio** (cambio palla)
2. Il punteggio è diverso da 0-0
**Limitazione cambio palla manuale:**
- Il cambio manuale del servizio (`Ctrl + ←`) è consentito **solo a 0-0**
- Questa limitazione previene errori nella rotazione delle formazioni
### Formazione in Campo
Visualizzazione a 6 posizioni standard:
```
Rete
┌─────┬─────┬─────┐
│ 4 │ 3 │ 2 │ ← Fila anteriore
├─────┼─────┼─────┤
│ 5 │ 6 │ 1 │ ← Fila posteriore
└─────┴─────┴─────┘
```
La rotazione avviene in senso orario: 1→2→3→4→5→6→1
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

View File

@@ -1,9 +1,52 @@
<section class="homepage"> <section class="homepage">
<w-dialog v-model="diaNomi.show" :width="500" @close="abilitaTastiSpeciali()"> <w-dialog v-model="diaNomi.show" :width="600" @close="abilitaTastiSpeciali()">
<w-input v-model="sp.nomi.home" type="text" class="pa3">Home</w-input> <w-input v-model="sp.nomi.home" type="text" class="pa3">Nome Home</w-input>
<w-input v-model="sp.nomi.guest" type="text" class="pa3">Guest</w-input> <w-input v-model="sp.nomi.guest" type="text" class="pa3">Nome Guest</w-input>
<w-button @click="order = !order">Inverti ordine</w-button>
<w-button bg-color="success" @click="diaNomi.show = false"> <w-flex justify-space-around class="pa3">
<div class="campo-config">
<div class="text-bold mb3 text-center">Formazione Home</div>
<div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore">
<w-input v-model="sp.form.home[3]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[2]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[1]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
<!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore">
<w-input v-model="sp.form.home[4]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[5]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.home[0]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
</div>
</div>
<div class="campo-config">
<div class="text-bold mb3 text-center">Formazione Guest</div>
<div class="campo-pallavolo">
<!-- Fila anteriore - index [3, 2, 1] - VICINO ALLA RETE (prima fila visualizzata) -->
<w-flex justify-center class="fila-anteriore">
<w-input v-model="sp.form.guest[3]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[2]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[1]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
<!-- Linea dei 3 metri -->
<div class="linea-tre-metri"></div>
<!-- Fila posteriore - index [4, 5, 0] - ZONA DIFESA (seconda fila visualizzata) -->
<w-flex justify-center class="fila-posteriore">
<w-input v-model="sp.form.guest[4]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[5]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
<w-input v-model="sp.form.guest[0]" type="text" style="width: 50px; text-align: center;" class="ma1"></w-input>
</w-flex>
</div>
</div>
</w-flex>
<w-button @click="order = !order" class="ma2">Inverti ordine</w-button>
<w-button bg-color="success" @click="diaNomi.show = false" class="ma2">
Ok Ok
</w-button> </w-button>
</w-dialog> </w-dialog>
@@ -106,7 +149,7 @@
<w-button @click="apriDialogConfig()"> <w-button @click="apriDialogConfig()">
<img src="/gear.png" width="25" /> <img src="/gear.png" width="25" />
</w-button> </w-button>
<w-button @click="sp.servHome = !sp.servHome"> <w-button @click="cambiaPalla" :disabled="!isPunteggioZeroZero">
<img src="/serv.png" width="25" /> <img src="/serv.png" width="25" />
</w-button> </w-button>
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta"> <w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">

View File

@@ -24,6 +24,7 @@ export default {
home: ["1", "2", "3", "4", "5", "6"], home: ["1", "2", "3", "4", "5", "6"],
guest: ["1", "2", "3", "4", "5", "6"], guest: ["1", "2", "3", "4", "5", "6"],
}, },
servizioPrecedente: [], // Stack per tracciare i cambi palla
}, },
} }
}, },
@@ -37,6 +38,11 @@ export default {
} }
this.abilitaTastiSpeciali(); this.abilitaTastiSpeciali();
}, },
computed: {
isPunteggioZeroZero() {
return this.sp.punt.home === 0 && this.sp.punt.guest === 0;
}
},
methods: { methods: {
closeApp() { closeApp() {
var win = window.open("", "_self"); var win = window.open("", "_self");
@@ -66,6 +72,14 @@ export default {
guest: ["1", "2", "3", "4", "5", "6"], guest: ["1", "2", "3", "4", "5", "6"],
} }
this.sp.striscia = { home: [0], guest: [0] } this.sp.striscia = { home: [0], guest: [0] }
this.sp.servizioPrecedente = []
},
cambiaPalla() {
if (!this.isPunteggioZeroZero) {
this.$waveui.notify("Cambio palla consentito solo a inizio set (0-0)", "warning");
return;
}
this.sp.servHome = !this.sp.servHome;
}, },
incSet(team) { incSet(team) {
if (this.sp.set[team] == 2) { if (this.sp.set[team] == 2) {
@@ -75,6 +89,12 @@ export default {
} }
}, },
incPunt(team) { incPunt(team) {
// Controlla se c'è già una condizione di vittoria
if (this.checkVittoria()) {
this.$waveui.notify("Il set è terminato!", "warning");
return;
}
this.sp.punt[team]++; this.sp.punt[team]++;
if (team == 'home') { if (team == 'home') {
this.sp.striscia.home.push(this.sp.punt.home) this.sp.striscia.home.push(this.sp.punt.home)
@@ -83,21 +103,58 @@ export default {
this.sp.striscia.guest.push(this.sp.punt.guest) this.sp.striscia.guest.push(this.sp.punt.guest)
this.sp.striscia.home.push(' ') this.sp.striscia.home.push(' ')
} }
this.sp.servHome = (team == "home");
// Ruota la formazione solo se c'è cambio palla (conquista del servizio)
const cambioPalla = (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome);
this.sp.servizioPrecedente.push(cambioPalla); // Salva se c'è stato cambio palla
if (cambioPalla) {
this.sp.form[team].push(this.sp.form[team].shift()); this.sp.form[team].push(this.sp.form[team].shift());
}
this.sp.servHome = (team == "home");
},
checkVittoria() {
const puntHome = this.sp.punt.home;
const puntGuest = this.sp.punt.guest;
const setHome = this.sp.set.home;
const setGuest = this.sp.set.guest;
const totSet = setHome + setGuest;
// Determina se siamo nel set decisivo (5° set)
const isSetDecisivo = totSet >= 4;
const punteggioVittoria = isSetDecisivo ? 15 : 25;
// Vittoria con punteggio >= 25 (o 15 per set decisivo) e almeno 2 punti di vantaggio
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) {
return true; // Home ha vinto
}
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) {
return true; // Guest ha vinto
}
return false;
}, },
decPunt() { decPunt() {
if (this.sp.striscia.home.length > 1) { if (this.sp.striscia.home.length > 1) {
var tmpHome = this.sp.striscia.home.pop() var tmpHome = this.sp.striscia.home.pop()
var tmpGuest = this.sp.striscia.guest.pop() var tmpGuest = this.sp.striscia.guest.pop()
var cambioPalla = this.sp.servizioPrecedente.pop() // Recupera se c'era stato cambio palla
if (tmpHome == ' ') { if (tmpHome == ' ') {
this.sp.punt.guest-- this.sp.punt.guest--
// Ruota indietro solo se c'era stato un cambio palla
if (cambioPalla) {
this.sp.form.guest.unshift(this.sp.form.guest.pop()); this.sp.form.guest.unshift(this.sp.form.guest.pop());
}
} else { } else {
this.sp.punt.home-- this.sp.punt.home--
// Ruota indietro solo se c'era stato un cambio palla
if (cambioPalla) {
this.sp.form.home.unshift(this.sp.form.home.pop()); this.sp.form.home.unshift(this.sp.form.home.pop());
} }
} }
}
}, },
// decPunt(team) { // decPunt(team) {
// // decrementa il punteggio se è > 0. // // decrementa il punteggio se è > 0.
@@ -171,7 +228,7 @@ export default {
} else if (e.shiftKey && e.key == "ArrowRight") { } else if (e.shiftKey && e.key == "ArrowRight") {
this.incSet("guest") this.incSet("guest")
} else if (e.ctrlKey && e.key == "ArrowLeft") { } else if (e.ctrlKey && e.key == "ArrowLeft") {
this.sp.servHome = !this.sp.servHome this.cambiaPalla()
} else { return false } } else { return false }
} }
} }

View File

@@ -122,3 +122,43 @@ button:focus-visible {
color: blue; color: blue;
border-radius: 5px; border-radius: 5px;
} }
.campo-config {
display: flex;
flex-direction: column;
align-items: center;
}
.campo-pallavolo {
border: 3px solid #999;
background-color: rgba(205, 133, 63, 0.25);
position: relative;
width: 220px;
height: 220px;
display: flex;
flex-direction: column;
padding: 0;
}
.fila-anteriore {
height: 33.33%;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.fila-posteriore {
height: 66.67%;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.linea-tre-metri {
border-top: 2px solid #666;
width: 100%;
height: 0;
margin: 0;
}