Compare commits
21 Commits
apk
...
a40fad7194
| Author | SHA1 | Date | |
|---|---|---|---|
| a40fad7194 | |||
| 3789f25d0d | |||
| d3698a506d | |||
| 1972fd37a4 | |||
| ea4d8ec523 | |||
| f190db2161 | |||
| 9df74a760f | |||
| 44617f2f86 | |||
| 33a1534319 | |||
| 2e66a6cf2a | |||
| c923bdbf64 | |||
| 139dcc9c5b | |||
| 24dda41b0d | |||
| 4cbb5fb48d | |||
| eae5cbf964 | |||
| 2c6416bfe0 | |||
| 9a808e566d | |||
| 6c6ac7fc29 | |||
| bbe0862241 | |||
| 26d647dce7 | |||
| a72bc1844e |
257
README.md
257
README.md
@@ -1,8 +1,255 @@
|
||||
# Vue 3 + Vite
|
||||
# nvm use v20.2.0
|
||||
# Segnapunti Anto
|
||||
|
||||
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
|
||||
---
|
||||
|
||||
- [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).
|
||||
## 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 (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**
|
||||
- Scorciatoie da tastiera complete (vedi sezione [Shortcuts](#shortcuts))
|
||||
- Sintesi vocale per annunci punteggio in italiano (Web Speech API)
|
||||
|
||||
- **Personalizzazione**
|
||||
- Configurazione dinamica nomi squadre
|
||||
- Selettore modalità partita: al meglio di 3 o al meglio di 5
|
||||
- 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 |
|
||||
| `Ctrl + C` | Apri dialog cambi |
|
||||
|
||||
### Controlli Tastiera Squadra Guest
|
||||
|
||||
| Scorciatoia | Azione |
|
||||
|-------------|--------|
|
||||
| `Shift + ↑` | Incrementa punti |
|
||||
| `Shift + ↓` | Decrementa punti |
|
||||
| `Shift + →` | Incrementa set |
|
||||
| `Shift + C` | Apri dialog cambi |
|
||||
|
||||
### 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**:
|
||||
- Modalità 2/3: 3° set a 15 punti con almeno 2 di vantaggio
|
||||
- Modalità 3/5: 5° set 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
|
||||
|
||||
1019
package-lock.json
generated
1019
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,12 +6,17 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"start": "node server.js",
|
||||
"serve": "vite build && node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^5.2.1",
|
||||
"nosleep.js": "^0.12.0",
|
||||
"vue": "^3.2.47",
|
||||
"wave-ui": "^3.3.0"
|
||||
"vue-router": "^4.6.4",
|
||||
"wave-ui": "^3.3.0",
|
||||
"ws": "^8.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.1.0",
|
||||
|
||||
235
server.js
Normal file
235
server.js
Normal file
@@ -0,0 +1,235 @@
|
||||
import { createServer } from 'http'
|
||||
import express from 'express'
|
||||
import { WebSocketServer } from 'ws'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { dirname, join } from 'path'
|
||||
|
||||
// Import shared game logic
|
||||
// We need to read it as a dynamic import since it uses ES modules
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = dirname(__filename)
|
||||
|
||||
// Inline the game state logic for the server (avoid complex ESM import from src/)
|
||||
function createInitialState() {
|
||||
return {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5",
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function checkVittoria(state) {
|
||||
const puntHome = state.sp.punt.home
|
||||
const puntGuest = state.sp.punt.guest
|
||||
const setHome = state.sp.set.home
|
||||
const setGuest = state.sp.set.guest
|
||||
const totSet = setHome + setGuest
|
||||
let isSetDecisivo = false
|
||||
if (state.modalitaPartita === "2/3") {
|
||||
isSetDecisivo = totSet >= 2
|
||||
} else {
|
||||
isSetDecisivo = totSet >= 4
|
||||
}
|
||||
const punteggioVittoria = isSetDecisivo ? 15 : 25
|
||||
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) return true
|
||||
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function applyAction(state, action) {
|
||||
const s = JSON.parse(JSON.stringify(state))
|
||||
switch (action.type) {
|
||||
case "incPunt": {
|
||||
const team = action.team
|
||||
if (checkVittoria(s)) break
|
||||
s.sp.storicoServizio.push({
|
||||
servHome: s.sp.servHome,
|
||||
cambioPalla: (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome),
|
||||
})
|
||||
s.sp.punt[team]++
|
||||
if (team === "home") {
|
||||
s.sp.striscia.home.push(s.sp.punt.home)
|
||||
s.sp.striscia.guest.push(" ")
|
||||
} else {
|
||||
s.sp.striscia.guest.push(s.sp.punt.guest)
|
||||
s.sp.striscia.home.push(" ")
|
||||
}
|
||||
const cambioPalla = (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome)
|
||||
if (cambioPalla) {
|
||||
s.sp.form[team].push(s.sp.form[team].shift())
|
||||
}
|
||||
s.sp.servHome = team === "home"
|
||||
break
|
||||
}
|
||||
case "decPunt": {
|
||||
if (s.sp.striscia.home.length > 1 && s.sp.storicoServizio.length > 0) {
|
||||
const tmpHome = s.sp.striscia.home.pop()
|
||||
s.sp.striscia.guest.pop()
|
||||
const statoServizio = s.sp.storicoServizio.pop()
|
||||
if (tmpHome === " ") {
|
||||
s.sp.punt.guest--
|
||||
if (statoServizio.cambioPalla) {
|
||||
s.sp.form.guest.unshift(s.sp.form.guest.pop())
|
||||
}
|
||||
} else {
|
||||
s.sp.punt.home--
|
||||
if (statoServizio.cambioPalla) {
|
||||
s.sp.form.home.unshift(s.sp.form.home.pop())
|
||||
}
|
||||
}
|
||||
s.sp.servHome = statoServizio.servHome
|
||||
}
|
||||
break
|
||||
}
|
||||
case "incSet": {
|
||||
const team = action.team
|
||||
if (s.sp.set[team] === 2) { s.sp.set[team] = 0 } else { s.sp.set[team]++ }
|
||||
break
|
||||
}
|
||||
case "cambiaPalla": {
|
||||
if (s.sp.punt.home === 0 && s.sp.punt.guest === 0) {
|
||||
s.sp.servHome = !s.sp.servHome
|
||||
}
|
||||
break
|
||||
}
|
||||
case "resetta": {
|
||||
s.visuForm = false
|
||||
s.sp.punt.home = 0
|
||||
s.sp.punt.guest = 0
|
||||
s.sp.form = {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
s.sp.striscia = { home: [0], guest: [0] }
|
||||
s.sp.storicoServizio = []
|
||||
break
|
||||
}
|
||||
case "toggleFormazione": { s.visuForm = !s.visuForm; break }
|
||||
case "toggleStriscia": { s.visuStriscia = !s.visuStriscia; break }
|
||||
case "toggleOrder": { s.order = !s.order; break }
|
||||
case "setNomi": {
|
||||
if (action.home !== undefined) s.sp.nomi.home = action.home
|
||||
if (action.guest !== undefined) s.sp.nomi.guest = action.guest
|
||||
break
|
||||
}
|
||||
case "setModalita": { s.modalitaPartita = action.modalita; break }
|
||||
case "setFormazione": {
|
||||
if (action.team && action.form) {
|
||||
s.sp.form[action.team] = [...action.form]
|
||||
}
|
||||
break
|
||||
}
|
||||
case "confermaCambi": {
|
||||
const team = action.team
|
||||
const cambi = action.cambi || []
|
||||
const form = s.sp.form[team].map((val) => String(val).trim())
|
||||
const formAggiornata = [...form]
|
||||
let valid = true
|
||||
for (const cambio of cambi) {
|
||||
const cin = (cambio.in || "").trim()
|
||||
const cout = (cambio.out || "").trim()
|
||||
if (!cin || !cout) continue
|
||||
if (!/^\d+$/.test(cin) || !/^\d+$/.test(cout)) { valid = false; break }
|
||||
if (cin === cout) { valid = false; break }
|
||||
if (formAggiornata.includes(cin)) { valid = false; break }
|
||||
if (!formAggiornata.includes(cout)) { valid = false; break }
|
||||
const idx = formAggiornata.findIndex((val) => String(val).trim() === cout)
|
||||
if (idx !== -1) { formAggiornata.splice(idx, 1, cin) }
|
||||
}
|
||||
if (valid) { s.sp.form[team] = formAggiornata }
|
||||
break
|
||||
}
|
||||
default: break
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ——— Server Setup ———
|
||||
|
||||
const app = express()
|
||||
const PORT = process.env.PORT || 3000
|
||||
|
||||
// Serve the Vite build output
|
||||
app.use(express.static(join(__dirname, 'dist')))
|
||||
|
||||
// SPA fallback: serve index.html for all non-file routes
|
||||
app.get('/{*splat}', (req, res) => {
|
||||
res.sendFile(join(__dirname, 'dist', 'index.html'))
|
||||
})
|
||||
|
||||
const server = createServer(app)
|
||||
|
||||
// WebSocket server
|
||||
const wss = new WebSocketServer({ server })
|
||||
|
||||
// Global game state
|
||||
let gameState = createInitialState()
|
||||
|
||||
// Track client roles
|
||||
const clients = new Map() // ws -> { role: 'display' | 'controller' }
|
||||
|
||||
wss.on('connection', (ws) => {
|
||||
console.log('New WebSocket connection')
|
||||
|
||||
ws.on('message', (data) => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString())
|
||||
|
||||
if (msg.type === 'register') {
|
||||
clients.set(ws, { role: msg.role || 'display' })
|
||||
console.log(`Client registered as: ${msg.role || 'display'}`)
|
||||
// Send current state immediately
|
||||
ws.send(JSON.stringify({ type: 'state', state: gameState }))
|
||||
return
|
||||
}
|
||||
|
||||
if (msg.type === 'action') {
|
||||
// Only controllers can send actions
|
||||
const client = clients.get(ws)
|
||||
if (!client || client.role !== 'controller') {
|
||||
console.log('Action rejected: not a controller')
|
||||
return
|
||||
}
|
||||
|
||||
// Apply the action to game state
|
||||
gameState = applyAction(gameState, msg.action)
|
||||
|
||||
// Broadcast new state to ALL connected clients
|
||||
const stateMsg = JSON.stringify({ type: 'state', state: gameState })
|
||||
wss.clients.forEach((c) => {
|
||||
if (c.readyState === 1) { // WebSocket.OPEN
|
||||
c.send(stateMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error processing message:', err)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('close', () => {
|
||||
clients.delete(ws)
|
||||
console.log('Client disconnected')
|
||||
})
|
||||
})
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`\n🏐 Segnapunti Server running on:`)
|
||||
console.log(` Display: http://localhost:${PORT}/`)
|
||||
console.log(` Controller: http://localhost:${PORT}/controller`)
|
||||
console.log(`\n Per accedere da altri dispositivi sulla rete locale,`)
|
||||
console.log(` usa l'IP di questo computer, es: http://192.168.1.x:${PORT}/controller\n`)
|
||||
})
|
||||
@@ -1,7 +1,3 @@
|
||||
<script setup>
|
||||
import HomePage from './components/HomePage/index.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<HomePage />
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
704
src/components/ControllerPage.vue
Normal file
704
src/components/ControllerPage.vue
Normal file
@@ -0,0 +1,704 @@
|
||||
<template>
|
||||
<section class="controller-page">
|
||||
<!-- Connection status bar -->
|
||||
<div class="conn-bar" :class="{ connected: wsConnected }">
|
||||
<span class="dot"></span>
|
||||
{{ wsConnected ? 'Connesso' : 'Connessione...' }}
|
||||
</div>
|
||||
|
||||
<!-- Score preview -->
|
||||
<div class="score-preview">
|
||||
<div class="team-score home-bg" @click="sendAction({ type: 'incPunt', team: 'home' })">
|
||||
<div class="team-name">{{ state.sp.nomi.home }}</div>
|
||||
<div class="team-pts">{{ state.sp.punt.home }}</div>
|
||||
<div class="team-set">SET {{ state.sp.set.home }}</div>
|
||||
<img v-show="state.sp.servHome" src="/serv.png" class="serv-icon" />
|
||||
</div>
|
||||
<div class="team-score guest-bg" @click="sendAction({ type: 'incPunt', team: 'guest' })">
|
||||
<div class="team-name">{{ state.sp.nomi.guest }}</div>
|
||||
<div class="team-pts">{{ state.sp.punt.guest }}</div>
|
||||
<div class="team-set">SET {{ state.sp.set.guest }}</div>
|
||||
<img v-show="!state.sp.servHome" src="/serv.png" class="serv-icon" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Undo row -->
|
||||
<div class="undo-row">
|
||||
<button class="btn btn-undo" @click="sendAction({ type: 'decPunt' })">
|
||||
↩ ANNULLA PUNTO
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Set buttons -->
|
||||
<div class="action-row">
|
||||
<button class="btn btn-set home-bg" @click="sendAction({ type: 'incSet', team: 'home' })">
|
||||
SET {{ state.sp.nomi.home }}
|
||||
</button>
|
||||
<button class="btn btn-set guest-bg" @click="sendAction({ type: 'incSet', team: 'guest' })">
|
||||
SET {{ state.sp.nomi.guest }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="controls">
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'cambiaPalla' })" :disabled="!isPunteggioZeroZero">
|
||||
🏐 Cambio Palla
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleFormazione' })">
|
||||
{{ state.visuForm ? '🔢 Punteggio' : '📋 Formazioni' }}
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleStriscia' })">
|
||||
📊 Striscia
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="sendAction({ type: 'toggleOrder' })">
|
||||
🔄 Inverti
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="speak()">
|
||||
🔊 Voce
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="openConfig()">
|
||||
⚙️ Config
|
||||
</button>
|
||||
<button class="btn btn-ctrl" @click="openCambiTeam()">
|
||||
🔀 Cambi
|
||||
</button>
|
||||
<button class="btn btn-danger" @click="confirmReset = true">
|
||||
🗑️ Reset
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Reset confirmation -->
|
||||
<div class="overlay" v-if="confirmReset" @click.self="confirmReset = false">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">Azzero punteggio?</div>
|
||||
<div class="dialog-buttons">
|
||||
<button class="btn btn-cancel" @click="confirmReset = false">NO</button>
|
||||
<button class="btn btn-confirm" @click="doReset()">SI</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Config dialog -->
|
||||
<div class="overlay" v-if="showConfig" @click.self="showConfig = false">
|
||||
<div class="dialog dialog-config">
|
||||
<div class="dialog-title">Configurazione</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Nome Home</label>
|
||||
<input type="text" v-model="configData.nomeHome" class="input-field" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Nome Guest</label>
|
||||
<input type="text" v-model="configData.nomeGuest" class="input-field" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Modalità partita</label>
|
||||
<div class="mode-buttons">
|
||||
<button :class="['btn', 'btn-mode', configData.modalita === '2/3' ? 'active' : '']"
|
||||
@click="configData.modalita = '2/3'">2/3</button>
|
||||
<button :class="['btn', 'btn-mode', configData.modalita === '3/5' ? 'active' : '']"
|
||||
@click="configData.modalita = '3/5'">3/5</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Formazione Home</label>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<input type="text" v-model="configData.formHome[3]" class="input-num" />
|
||||
<input type="text" v-model="configData.formHome[2]" class="input-num" />
|
||||
<input type="text" v-model="configData.formHome[1]" class="input-num" />
|
||||
</div>
|
||||
<div class="form-line"></div>
|
||||
<div class="form-row">
|
||||
<input type="text" v-model="configData.formHome[4]" class="input-num" />
|
||||
<input type="text" v-model="configData.formHome[5]" class="input-num" />
|
||||
<input type="text" v-model="configData.formHome[0]" class="input-num" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Formazione Guest</label>
|
||||
<div class="form-grid">
|
||||
<div class="form-row">
|
||||
<input type="text" v-model="configData.formGuest[3]" class="input-num" />
|
||||
<input type="text" v-model="configData.formGuest[2]" class="input-num" />
|
||||
<input type="text" v-model="configData.formGuest[1]" class="input-num" />
|
||||
</div>
|
||||
<div class="form-line"></div>
|
||||
<div class="form-row">
|
||||
<input type="text" v-model="configData.formGuest[4]" class="input-num" />
|
||||
<input type="text" v-model="configData.formGuest[5]" class="input-num" />
|
||||
<input type="text" v-model="configData.formGuest[0]" class="input-num" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dialog-buttons">
|
||||
<button class="btn btn-cancel" @click="showConfig = false">Annulla</button>
|
||||
<button class="btn btn-confirm" @click="saveConfig()">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cambi team selection -->
|
||||
<div class="overlay" v-if="showCambiTeam" @click.self="showCambiTeam = false">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">Scegli squadra</div>
|
||||
<div class="dialog-buttons">
|
||||
<button class="btn btn-set home-bg" @click="openCambi('home')">{{ state.sp.nomi.home }}</button>
|
||||
<button class="btn btn-set guest-bg" @click="openCambi('guest')">{{ state.sp.nomi.guest }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cambi dialog -->
|
||||
<div class="overlay" v-if="showCambi" @click.self="closeCambi()">
|
||||
<div class="dialog">
|
||||
<div class="dialog-title">{{ state.sp.nomi[cambiTeam] }}: CAMBIO</div>
|
||||
<div class="cambi-container">
|
||||
<div class="cambio-row" v-for="(c, i) in cambiData" :key="i">
|
||||
<input type="text" v-model="c.in" placeholder="IN" class="input-num cambi-in-field" />
|
||||
<span class="cambio-arrow">→</span>
|
||||
<input type="text" v-model="c.out" placeholder="OUT" class="input-num cambi-out-field" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-buttons">
|
||||
<button class="btn btn-cancel" @click="closeCambi()">Annulla</button>
|
||||
<button class="btn btn-confirm" :disabled="!cambiValid" @click="confermaCambi()">CONFERMA</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ControllerPage",
|
||||
data() {
|
||||
return {
|
||||
ws: null,
|
||||
wsConnected: false,
|
||||
confirmReset: false,
|
||||
showConfig: false,
|
||||
showCambiTeam: false,
|
||||
showCambi: false,
|
||||
cambiTeam: "home",
|
||||
cambiData: [
|
||||
{ in: "", out: "" },
|
||||
{ in: "", out: "" },
|
||||
],
|
||||
configData: {
|
||||
nomeHome: "",
|
||||
nomeGuest: "",
|
||||
modalita: "3/5",
|
||||
formHome: ["1", "2", "3", "4", "5", "6"],
|
||||
formGuest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
state: {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5",
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isPunteggioZeroZero() {
|
||||
return this.state.sp.punt.home === 0 && this.state.sp.punt.guest === 0
|
||||
},
|
||||
cambiValid() {
|
||||
let hasComplete = false
|
||||
let allValid = true
|
||||
this.cambiData.forEach((c) => {
|
||||
const cin = (c.in || "").trim()
|
||||
const cout = (c.out || "").trim()
|
||||
if (!cin && !cout) return
|
||||
if (!cin || !cout) { allValid = false; return }
|
||||
hasComplete = true
|
||||
})
|
||||
return allValid && hasComplete
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connectWebSocket()
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.ws) this.ws.close()
|
||||
},
|
||||
methods: {
|
||||
connectWebSocket() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${location.host}`
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.wsConnected = true
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'controller' }))
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg.type === 'state') {
|
||||
this.state = msg.state
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('WS parse error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.wsConnected = false
|
||||
setTimeout(() => this.connectWebSocket(), 2000)
|
||||
}
|
||||
|
||||
this.ws.onerror = () => { this.wsConnected = false }
|
||||
},
|
||||
|
||||
sendAction(action) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify({ type: 'action', action }))
|
||||
}
|
||||
},
|
||||
|
||||
doReset() {
|
||||
this.sendAction({ type: 'resetta' })
|
||||
this.confirmReset = false
|
||||
},
|
||||
|
||||
openConfig() {
|
||||
this.configData.nomeHome = this.state.sp.nomi.home
|
||||
this.configData.nomeGuest = this.state.sp.nomi.guest
|
||||
this.configData.modalita = this.state.modalitaPartita
|
||||
this.configData.formHome = [...this.state.sp.form.home]
|
||||
this.configData.formGuest = [...this.state.sp.form.guest]
|
||||
this.showConfig = true
|
||||
},
|
||||
|
||||
saveConfig() {
|
||||
this.sendAction({ type: 'setNomi', home: this.configData.nomeHome, guest: this.configData.nomeGuest })
|
||||
this.sendAction({ type: 'setModalita', modalita: this.configData.modalita })
|
||||
this.sendAction({ type: 'setFormazione', team: 'home', form: this.configData.formHome })
|
||||
this.sendAction({ type: 'setFormazione', team: 'guest', form: this.configData.formGuest })
|
||||
this.showConfig = false
|
||||
},
|
||||
|
||||
openCambiTeam() {
|
||||
this.showCambiTeam = true
|
||||
},
|
||||
|
||||
openCambi(team) {
|
||||
this.showCambiTeam = false
|
||||
this.cambiTeam = team
|
||||
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
|
||||
this.showCambi = true
|
||||
},
|
||||
|
||||
closeCambi() {
|
||||
this.showCambi = false
|
||||
this.cambiData = [{ in: "", out: "" }, { in: "", out: "" }]
|
||||
},
|
||||
|
||||
confermaCambi() {
|
||||
if (!this.cambiValid) return
|
||||
const cambi = this.cambiData
|
||||
.filter(c => (c.in || "").trim() && (c.out || "").trim())
|
||||
.map(c => ({ in: c.in.trim(), out: c.out.trim() }))
|
||||
this.sendAction({ type: 'confermaCambi', team: this.cambiTeam, cambi })
|
||||
this.closeCambi()
|
||||
},
|
||||
|
||||
speak() {
|
||||
const msg = new SpeechSynthesisUtterance()
|
||||
if (this.state.sp.punt.home + this.state.sp.punt.guest === 0) {
|
||||
msg.text = "zero a zero"
|
||||
} else if (this.state.sp.punt.home === this.state.sp.punt.guest) {
|
||||
msg.text = this.state.sp.punt.home + " pari"
|
||||
} else {
|
||||
if (this.state.sp.servHome) {
|
||||
msg.text = this.state.sp.punt.home + " a " + this.state.sp.punt.guest
|
||||
} else {
|
||||
msg.text = this.state.sp.punt.guest + " a " + this.state.sp.punt.home
|
||||
}
|
||||
}
|
||||
const voices = window.speechSynthesis.getVoices()
|
||||
msg.voice = voices.find(v => v.name === 'Google italiano')
|
||||
window.speechSynthesis.speak(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.controller-page {
|
||||
min-height: 100vh;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
padding-top: 36px;
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Connection bar */
|
||||
.conn-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
background: #c62828;
|
||||
color: white;
|
||||
z-index: 200;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.conn-bar.connected {
|
||||
background: #2e7d32;
|
||||
}
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Score preview */
|
||||
.score-preview {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.team-score {
|
||||
flex: 1;
|
||||
border-radius: 16px;
|
||||
padding: 16px 12px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: transform 0.1s;
|
||||
min-height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.team-score:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
.home-bg {
|
||||
background: linear-gradient(145deg, #1a1a1a, #333);
|
||||
border: 2px solid #fdd835;
|
||||
color: #fdd835;
|
||||
}
|
||||
.guest-bg {
|
||||
background: linear-gradient(145deg, #0d47a1, #1565c0);
|
||||
border: 2px solid #64b5f6;
|
||||
color: #fff;
|
||||
}
|
||||
.team-name {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.team-pts {
|
||||
font-size: 56px;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
.team-set {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
opacity: 0.75;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.serv-icon {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
/* Undo row */
|
||||
.undo-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.btn-undo {
|
||||
width: 100%;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: #ffab91;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-undo:active {
|
||||
background: rgba(255,100,50,0.2);
|
||||
}
|
||||
|
||||
/* Set buttons */
|
||||
.action-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.btn-set {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.btn-set:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
/* Controls grid */
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.btn-ctrl {
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
color: #e0e0e0;
|
||||
padding: 14px 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.btn-ctrl:active {
|
||||
background: rgba(255,255,255,0.18);
|
||||
}
|
||||
.btn-ctrl:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: rgba(198, 40, 40, 0.25);
|
||||
border: 1px solid rgba(239, 83, 80, 0.4);
|
||||
color: #ef5350;
|
||||
padding: 14px 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.btn-danger:active {
|
||||
background: rgba(198, 40, 40, 0.45);
|
||||
}
|
||||
|
||||
/* Overlays & Dialogs */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.75);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 300;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dialog {
|
||||
background: #1e1e1e;
|
||||
border-radius: 20px;
|
||||
padding: 24px;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
.dialog-config {
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.12);
|
||||
}
|
||||
|
||||
.dialog-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
flex: 1;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: #aaa;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
flex: 1;
|
||||
background: #2e7d32;
|
||||
color: white;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.btn-confirm:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Form groups */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
margin-bottom: 6px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.input-field {
|
||||
width: 100%;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input-field:focus {
|
||||
outline: none;
|
||||
border-color: #64b5f6;
|
||||
}
|
||||
|
||||
.input-num {
|
||||
width: 52px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
color: white;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input-num:focus {
|
||||
outline: none;
|
||||
border-color: #64b5f6;
|
||||
}
|
||||
|
||||
/* Form grid */
|
||||
.form-grid {
|
||||
background: rgba(205, 133, 63, 0.15);
|
||||
border: 2px solid rgba(255,255,255,0.15);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
.form-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.form-line {
|
||||
border-top: 1px dashed rgba(255,255,255,0.3);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Mode buttons */
|
||||
.mode-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn-mode {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: #aaa;
|
||||
border-radius: 10px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.btn-mode.active {
|
||||
background: #2e7d32;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Cambi */
|
||||
.cambi-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.cambio-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.cambio-arrow {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #aaa;
|
||||
}
|
||||
.cambi-in-field {
|
||||
background: rgba(120, 200, 120, 0.2) !important;
|
||||
border-color: rgba(120, 200, 120, 0.4) !important;
|
||||
}
|
||||
.cambi-out-field {
|
||||
background: rgba(200, 120, 120, 0.2) !important;
|
||||
border-color: rgba(200, 120, 120, 0.4) !important;
|
||||
}
|
||||
</style>
|
||||
269
src/components/DisplayPage.vue
Normal file
269
src/components/DisplayPage.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<section class="display-page">
|
||||
<div class="campo">
|
||||
<span v-if="state.order">
|
||||
<!-- home guest -->
|
||||
<div class="hea home">
|
||||
<span :style="{ 'float': 'left' }">
|
||||
{{ state.sp.nomi.home }}
|
||||
<span class="serv-slot">
|
||||
<img v-show="state.sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.home }}</span>
|
||||
</span>
|
||||
<span class="mr3" :style="{ 'float': 'right' }">set {{ state.sp.set.home }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hea guest">
|
||||
<span :style="{ 'float': 'right' }">
|
||||
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.guest }}</span>
|
||||
<span class="serv-slot">
|
||||
<img v-show="!state.sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
{{ state.sp.nomi.guest }}
|
||||
</span>
|
||||
<span class="ml3" :style="{ 'float': 'left' }">set {{ state.sp.set.guest }}</span>
|
||||
</div>
|
||||
|
||||
<span v-if="state.visuForm">
|
||||
<div class="col form home">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'hf'+x">
|
||||
{{ state.sp.form.home[x] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form guest">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'gf'+x">
|
||||
{{ state.sp.form.guest[x] }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<div class="punteggio-container">
|
||||
<div class="col punt home">{{ state.sp.punt.home }}</div>
|
||||
<div class="col punt guest">{{ state.sp.punt.guest }}</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
<!-- guest home -->
|
||||
<div class="hea guest">
|
||||
<span :style="{ 'float': 'left' }">
|
||||
{{ state.sp.nomi.guest }}
|
||||
<span class="serv-slot">
|
||||
<img v-show="!state.sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.guest }}</span>
|
||||
</span>
|
||||
<span class="mr3" :style="{ 'float': 'right' }">set {{ state.sp.set.guest }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hea home">
|
||||
<span :style="{ 'float': 'right' }">
|
||||
<span v-if="state.visuForm" class="score-inline">{{ state.sp.punt.home }}</span>
|
||||
<span class="serv-slot">
|
||||
<img v-show="state.sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
{{ state.sp.nomi.home }}
|
||||
</span>
|
||||
<span class="ml3" :style="{ 'float': 'left' }">set {{ state.sp.set.home }}</span>
|
||||
</div>
|
||||
|
||||
<span v-if="state.visuForm">
|
||||
<div class="col form guest">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'gf2'+x">
|
||||
{{ state.sp.form.guest[x] }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col form home">
|
||||
<div class="formdiv" v-for="x in [3, 2, 1, 4, 5, 0]" :key="'hf2'+x">
|
||||
{{ state.sp.form.home[x] }}
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<div class="punteggio-container">
|
||||
<div class="col punt guest">{{ state.sp.punt.guest }}</div>
|
||||
<div class="col punt home">{{ state.sp.punt.home }}</div>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="striscia" v-if="state.visuStriscia">
|
||||
<div>
|
||||
<span class="text-bold mr1">{{ state.sp.nomi.home }}</span>
|
||||
<div v-for="(h, i) in state.sp.striscia.home" :key="'sh'+i" class="item">
|
||||
{{ String(h) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="guest-striscia">
|
||||
<span class="text-bold mr1">{{ state.sp.nomi.guest }}</span>
|
||||
<div v-for="(h, i) in state.sp.striscia.guest" :key="'sg'+i" class="item">
|
||||
{{ String(h) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Connection status indicator -->
|
||||
<div class="connection-status" :class="{ connected: wsConnected, disconnected: !wsConnected }">
|
||||
<span class="dot"></span>
|
||||
{{ wsConnected ? '' : 'Disconnesso' }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DisplayPage",
|
||||
data() {
|
||||
return {
|
||||
ws: null,
|
||||
wsConnected: false,
|
||||
state: {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5",
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.connectWebSocket()
|
||||
// Fullscreen on mobile
|
||||
if (this.isMobile()) {
|
||||
try { document.documentElement.requestFullscreen() } catch (e) {}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isMobile() {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
|
||||
},
|
||||
connectWebSocket() {
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${location.host}`
|
||||
this.ws = new WebSocket(wsUrl)
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.wsConnected = true
|
||||
// Register as display
|
||||
this.ws.send(JSON.stringify({ type: 'register', role: 'display' }))
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const msg = JSON.parse(event.data)
|
||||
if (msg.type === 'state') {
|
||||
this.state = msg.state
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error parsing WS message:', e)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.wsConnected = false
|
||||
// Auto-reconnect after 2 seconds
|
||||
setTimeout(() => this.connectWebSocket(), 2000)
|
||||
}
|
||||
|
||||
this.ws.onerror = () => {
|
||||
this.wsConnected = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.display-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.connection-status {
|
||||
position: fixed;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
z-index: 100;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.connection-status.connected {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.connection-status.disconnected {
|
||||
background: rgba(255, 50, 50, 0.8);
|
||||
color: white;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.connected .dot {
|
||||
background: #4caf50;
|
||||
}
|
||||
|
||||
.disconnected .dot {
|
||||
background: #f44336;
|
||||
animation: blink 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
.guest-striscia {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.punteggio-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.punt {
|
||||
font-size: 60vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 50vh;
|
||||
min-width: 50vw;
|
||||
max-width: 50vw;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -1,28 +1,126 @@
|
||||
<section class="homepage">
|
||||
<w-dialog v-model="diaNomi.show" :width="500" @close="abilitaTastiSpeciali()">
|
||||
<w-input v-model="sp.nomi.home" type="text" class="pa3">Home</w-input>
|
||||
<w-input v-model="sp.nomi.guest" type="text" class="pa3">Guest</w-input>
|
||||
<w-button @click="order = !order">Inverti ordine</w-button>
|
||||
<w-button bg-color="success" @click="diaNomi.show = false">
|
||||
<w-dialog v-model="diaNomi.show" :width="600" @close="chiudiDialogConfig()">
|
||||
<w-input v-model="sp.nomi.home" type="text" class="pa3" tabindex="1">Nome Home</w-input>
|
||||
<w-input v-model="sp.nomi.guest" type="text" class="pa3" tabindex="2">Nome Guest</w-input>
|
||||
|
||||
<w-flex justify-center align-center class="pa3">
|
||||
<span class="mr3">Modalità partita:</span>
|
||||
<w-button
|
||||
@click="modalitaPartita = '2/3'"
|
||||
:bg-color="modalitaPartita === '2/3' ? 'success' : 'grey-light4'"
|
||||
:dark="modalitaPartita === '2/3'"
|
||||
class="ma1"
|
||||
tabindex="-1">
|
||||
2/3
|
||||
</w-button>
|
||||
<w-button
|
||||
@click="modalitaPartita = '3/5'"
|
||||
:bg-color="modalitaPartita === '3/5' ? 'success' : 'grey-light4'"
|
||||
:dark="modalitaPartita === '3/5'"
|
||||
class="ma1"
|
||||
tabindex="-1">
|
||||
3/5
|
||||
</w-button>
|
||||
</w-flex>
|
||||
|
||||
<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" tabindex="6"></w-input>
|
||||
<w-input v-model="sp.form.home[2]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="5"></w-input>
|
||||
<w-input v-model="sp.form.home[1]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="4"></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" tabindex="7"></w-input>
|
||||
<w-input v-model="sp.form.home[5]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="8"></w-input>
|
||||
<w-input v-model="sp.form.home[0]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="3"></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" tabindex="12"></w-input>
|
||||
<w-input v-model="sp.form.guest[2]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="11"></w-input>
|
||||
<w-input v-model="sp.form.guest[1]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="10"></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" tabindex="13"></w-input>
|
||||
<w-input v-model="sp.form.guest[5]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="14"></w-input>
|
||||
<w-input v-model="sp.form.guest[0]" type="text" style="width: 50px; text-align: center;" class="ma1" tabindex="9"></w-input>
|
||||
</w-flex>
|
||||
</div>
|
||||
</div>
|
||||
</w-flex>
|
||||
|
||||
<w-button @click="order = !order" class="ma2" tabindex="-1">Inverti ordine</w-button>
|
||||
<w-button bg-color="success" @click="diaNomi.show = false" class="ma2" tabindex="-1">
|
||||
Ok
|
||||
</w-button>
|
||||
</w-dialog>
|
||||
<w-dialog v-model="diaCambiTeam.show" :width="420" @close="abilitaTastiSpeciali()">
|
||||
<div class="text-bold text-center mb2">Scegli squadra</div>
|
||||
<w-flex justify-center class="pa3">
|
||||
<w-button class="ma2" @click="selezionaTeamCambi('home')">{{ sp.nomi.home }}</w-button>
|
||||
<w-button class="ma2" @click="selezionaTeamCambi('guest')">{{ sp.nomi.guest }}</w-button>
|
||||
</w-flex>
|
||||
</w-dialog>
|
||||
<w-dialog v-model="diaCambi.show" :width="360" @close="chiudiDialogCambi">
|
||||
<div class="cambi-dialog">
|
||||
<div class="cambi-title">{{ sp.nomi[diaCambi.team] }}: CAMBIO</div>
|
||||
<div class="cambi-rows">
|
||||
<div class="cambi-row">
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[0].in" type="text" class="cambi-input cambi-in"></w-input>
|
||||
<span class="cambi-arrow">→</span>
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[0].out" type="text" class="cambi-input cambi-out"></w-input>
|
||||
</div>
|
||||
<div class="cambi-row">
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[1].in" type="text" class="cambi-input cambi-in"></w-input>
|
||||
<span class="cambi-arrow">→</span>
|
||||
<w-input v-model="diaCambi[diaCambi.team].cambi[1].out" type="text" class="cambi-input cambi-out"></w-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<w-flex justify-end class="pa3">
|
||||
<w-button bg-color="success" :disabled="!cambiConfermabili" @click="confermaCambi">
|
||||
CONFERMA
|
||||
</w-button>
|
||||
</w-flex>
|
||||
</w-dialog>
|
||||
<div class="campo">
|
||||
|
||||
<span v-if="order">
|
||||
<!-- home guest -->
|
||||
<div class="hea home">
|
||||
<span @click="decPunt('home')" :style="{ 'float': 'left' }">
|
||||
{{ sp.nomi.home }} <img v-if="sp.servHome" src="/serv.png" width="25" />
|
||||
<span v-if="visuForm">{{ sp.punt.home }}</span>
|
||||
{{ sp.nomi.home }}
|
||||
<span class="serv-slot">
|
||||
<img v-show="sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
|
||||
</span>
|
||||
<span @click="incSet('home')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.home }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hea guest">
|
||||
<span @click="decPunt('guest')" :style="{ 'float': 'right' }">
|
||||
<img v-if="!sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.guest }}
|
||||
<span v-if="visuForm">{{ sp.punt.guest }}</span>
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
|
||||
<span class="serv-slot">
|
||||
<img v-show="!sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
{{ sp.nomi.guest }}
|
||||
</span>
|
||||
<span @click="incSet('guest')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.guest }}</span>
|
||||
</div>
|
||||
@@ -40,8 +138,10 @@
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<div class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</div>
|
||||
<div class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div>
|
||||
<w-flex class="punteggio-container">
|
||||
<w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
|
||||
<w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
|
||||
</w-flex>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
@@ -50,16 +150,22 @@
|
||||
|
||||
<div class="hea guest">
|
||||
<span @click="decPunt('guest')" :style="{ 'float': 'left' }">
|
||||
{{ sp.nomi.guest }} <img v-if="!sp.servHome" src="/serv.png" width="25" />
|
||||
<span v-if="visuForm">{{ sp.punt.guest }}</span>
|
||||
{{ sp.nomi.guest }}
|
||||
<span class="serv-slot">
|
||||
<img v-show="!sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.guest }}</span>
|
||||
</span>
|
||||
<span @click="incSet('guest')" class="mr3" :style="{ 'float': 'right' }">set {{ sp.set.guest }}</span>
|
||||
</div>
|
||||
|
||||
<div class="hea home">
|
||||
<span @click="decPunt('home')" :style="{ 'float': 'right' }">
|
||||
<img v-if="sp.servHome" src="/serv.png" width="25" /> {{ sp.nomi.home }}
|
||||
<span v-if="visuForm">{{ sp.punt.home }}</span>
|
||||
<span v-if="visuForm" class="score-inline">{{ sp.punt.home }}</span>
|
||||
<span class="serv-slot">
|
||||
<img v-show="sp.servHome" src="/serv.png" width="25" />
|
||||
</span>
|
||||
{{ sp.nomi.home }}
|
||||
</span>
|
||||
<span @click="incSet('home')" class="ml3" :style="{ 'float': 'left' }">set {{ sp.set.home }}</span>
|
||||
</div>
|
||||
@@ -77,8 +183,10 @@
|
||||
</div>
|
||||
</span>
|
||||
<span v-else>
|
||||
<div class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</div>
|
||||
<div class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</div>
|
||||
<w-flex class="punteggio-container">
|
||||
<w-flex justify-center align-center class="col punt guest" @click="incPunt('guest')">{{ sp.punt.guest }}</w-flex>
|
||||
<w-flex justify-center align-center class="col punt home" @click="incPunt('home')">{{ sp.punt.home }}</w-flex>
|
||||
</w-flex>
|
||||
</span>
|
||||
|
||||
</span>
|
||||
@@ -106,7 +214,7 @@
|
||||
<w-button @click="apriDialogConfig()">
|
||||
<img src="/gear.png" width="25" />
|
||||
</w-button>
|
||||
<w-button @click="sp.servHome = !sp.servHome">
|
||||
<w-button @click="cambiaPalla" :disabled="!isPunteggioZeroZero">
|
||||
<img src="/serv.png" width="25" />
|
||||
</w-button>
|
||||
<w-confirm top left question="Azzero punteggio ?" cancel="NO" confirm="SI" @confirm="resetta">
|
||||
@@ -116,6 +224,9 @@
|
||||
<span v-if="visuForm">PUNTEGGIO</span>
|
||||
<span v-if="!visuForm">FORMAZIONI</span>
|
||||
</w-button>
|
||||
<w-button @click="apriDialogCambi">
|
||||
CAMBI
|
||||
</w-button>
|
||||
<w-button @click="visuStriscia = !visuStriscia">
|
||||
STRISCIA
|
||||
</w-button>
|
||||
|
||||
@@ -11,9 +11,19 @@ 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,
|
||||
modalitaPartita: "3/5", // "2/3" o "3/5"
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
@@ -24,6 +34,7 @@ export default {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [], // Stack per tracciare lo stato del servizio prima di ogni punto
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -37,6 +48,33 @@ export default {
|
||||
}
|
||||
this.abilitaTastiSpeciali();
|
||||
},
|
||||
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: {
|
||||
closeApp() {
|
||||
var win = window.open("", "_self");
|
||||
@@ -66,6 +104,14 @@ export default {
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
this.sp.striscia = { home: [0], guest: [0] }
|
||||
this.sp.storicoServizio = []
|
||||
},
|
||||
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) {
|
||||
if (this.sp.set[team] == 2) {
|
||||
@@ -75,6 +121,17 @@ export default {
|
||||
}
|
||||
},
|
||||
incPunt(team) {
|
||||
// Se il set è già terminato, evita ulteriori incrementi
|
||||
if (this.checkVittoria()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Salva lo stato del servizio PRIMA di modificarlo
|
||||
this.sp.storicoServizio.push({
|
||||
servHome: this.sp.servHome,
|
||||
cambioPalla: (team == "home" && !this.sp.servHome) || (team == "guest" && this.sp.servHome)
|
||||
});
|
||||
|
||||
this.sp.punt[team]++;
|
||||
if (team == 'home') {
|
||||
this.sp.striscia.home.push(this.sp.punt.home)
|
||||
@@ -83,21 +140,69 @@ export default {
|
||||
this.sp.striscia.guest.push(this.sp.punt.guest)
|
||||
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);
|
||||
|
||||
if (cambioPalla) {
|
||||
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 in base alla modalità partita
|
||||
let isSetDecisivo = false;
|
||||
if (this.modalitaPartita === "2/3") {
|
||||
// Tie-break al 3° set (quando totSet >= 2)
|
||||
isSetDecisivo = totSet >= 2;
|
||||
} else {
|
||||
// Tie-break al 5° set (quando totSet >= 4)
|
||||
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() {
|
||||
if (this.sp.striscia.home.length > 1) {
|
||||
if (this.sp.striscia.home.length > 1 && this.sp.storicoServizio.length > 0) {
|
||||
var tmpHome = this.sp.striscia.home.pop()
|
||||
var tmpGuest = this.sp.striscia.guest.pop()
|
||||
var statoServizio = this.sp.storicoServizio.pop() // Recupera lo stato completo del servizio
|
||||
|
||||
if (tmpHome == ' ') {
|
||||
this.sp.punt.guest--
|
||||
// Ruota indietro solo se c'era stato un cambio palla
|
||||
if (statoServizio.cambioPalla) {
|
||||
this.sp.form.guest.unshift(this.sp.form.guest.pop());
|
||||
}
|
||||
} else {
|
||||
this.sp.punt.home--
|
||||
// Ruota indietro solo se c'era stato un cambio palla
|
||||
if (statoServizio.cambioPalla) {
|
||||
this.sp.form.home.unshift(this.sp.form.home.pop());
|
||||
}
|
||||
}
|
||||
|
||||
// Ripristina il servizio allo stato precedente
|
||||
this.sp.servHome = statoServizio.servHome;
|
||||
|
||||
}
|
||||
},
|
||||
// decPunt(team) {
|
||||
// // decrementa il punteggio se è > 0.
|
||||
@@ -139,6 +244,152 @@ export default {
|
||||
apriDialogConfig() {
|
||||
this.disabilitaTastiSpeciali();
|
||||
this.diaNomi.show = true;
|
||||
|
||||
// Aggiungi gestore Tab per il dialog
|
||||
this.dialogConfigTabHandler = (e) => {
|
||||
if (e.key === 'Tab' && this.diaNomi.show) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const dialog = document.querySelector('.w-dialog');
|
||||
if (!dialog) return;
|
||||
|
||||
const allInputs = Array.from(dialog.querySelectorAll('input[type="text"]'))
|
||||
.sort((a, b) => {
|
||||
const tabA = parseInt(a.closest('[tabindex]')?.getAttribute('tabindex') || '0');
|
||||
const tabB = parseInt(b.closest('[tabindex]')?.getAttribute('tabindex') || '0');
|
||||
return tabA - tabB;
|
||||
});
|
||||
|
||||
if (allInputs.length === 0) return;
|
||||
|
||||
// Verifica se il focus è già dentro il dialog
|
||||
const focusInDialog = dialog.contains(document.activeElement);
|
||||
|
||||
// Se non è nel dialog o non è in un input, vai al primo
|
||||
if (!focusInDialog || !allInputs.includes(document.activeElement)) {
|
||||
allInputs[0].focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Navigazione normale tra i campi
|
||||
const currentIndex = allInputs.indexOf(document.activeElement);
|
||||
let nextIndex;
|
||||
|
||||
if (e.shiftKey) {
|
||||
nextIndex = currentIndex <= 0 ? allInputs.length - 1 : currentIndex - 1;
|
||||
} else {
|
||||
nextIndex = currentIndex >= allInputs.length - 1 ? 0 : currentIndex + 1;
|
||||
}
|
||||
|
||||
if (allInputs[nextIndex]) {
|
||||
allInputs[nextIndex].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', this.dialogConfigTabHandler, true);
|
||||
|
||||
// Focus immediato + retry con timeout
|
||||
this.$nextTick(() => {
|
||||
const focusFirst = () => {
|
||||
const dialog = document.querySelector('.w-dialog');
|
||||
if (dialog) {
|
||||
const firstInput = dialog.querySelector('input[type="text"]');
|
||||
if (firstInput) {
|
||||
firstInput.focus();
|
||||
firstInput.select();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Prova immediatamente
|
||||
if (!focusFirst()) {
|
||||
// Se fallisce, riprova dopo un breve delay
|
||||
setTimeout(focusFirst, 100);
|
||||
}
|
||||
});
|
||||
},
|
||||
chiudiDialogConfig() {
|
||||
if (this.dialogConfigTabHandler) {
|
||||
window.removeEventListener('keydown', this.dialogConfigTabHandler, true);
|
||||
this.dialogConfigTabHandler = null;
|
||||
}
|
||||
this.abilitaTastiSpeciali();
|
||||
},
|
||||
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);
|
||||
@@ -147,32 +398,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.sp.servHome = !this.sp.servHome
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
204
src/gameState.js
Normal file
204
src/gameState.js
Normal file
@@ -0,0 +1,204 @@
|
||||
/**
|
||||
* Shared game logic for segnapunti.
|
||||
* Used by both the WebSocket server and the client-side for local preview.
|
||||
*/
|
||||
|
||||
export function createInitialState() {
|
||||
return {
|
||||
order: true,
|
||||
visuForm: false,
|
||||
visuStriscia: true,
|
||||
modalitaPartita: "3/5",
|
||||
sp: {
|
||||
striscia: { home: [0], guest: [0] },
|
||||
servHome: true,
|
||||
punt: { home: 0, guest: 0 },
|
||||
set: { home: 0, guest: 0 },
|
||||
nomi: { home: "Antoniana", guest: "Guest" },
|
||||
form: {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
},
|
||||
storicoServizio: [],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function checkVittoria(state) {
|
||||
const puntHome = state.sp.punt.home
|
||||
const puntGuest = state.sp.punt.guest
|
||||
const setHome = state.sp.set.home
|
||||
const setGuest = state.sp.set.guest
|
||||
const totSet = setHome + setGuest
|
||||
|
||||
let isSetDecisivo = false
|
||||
if (state.modalitaPartita === "2/3") {
|
||||
isSetDecisivo = totSet >= 2
|
||||
} else {
|
||||
isSetDecisivo = totSet >= 4
|
||||
}
|
||||
|
||||
const punteggioVittoria = isSetDecisivo ? 15 : 25
|
||||
|
||||
if (puntHome >= punteggioVittoria && puntHome - puntGuest >= 2) {
|
||||
return true
|
||||
}
|
||||
if (puntGuest >= punteggioVittoria && puntGuest - puntHome >= 2) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function applyAction(state, action) {
|
||||
// Deep-clone to avoid mutation issues (server-side)
|
||||
// Returns new state
|
||||
const s = JSON.parse(JSON.stringify(state))
|
||||
|
||||
switch (action.type) {
|
||||
case "incPunt": {
|
||||
const team = action.team
|
||||
if (checkVittoria(s)) break
|
||||
|
||||
s.sp.storicoServizio.push({
|
||||
servHome: s.sp.servHome,
|
||||
cambioPalla: (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome),
|
||||
})
|
||||
|
||||
s.sp.punt[team]++
|
||||
if (team === "home") {
|
||||
s.sp.striscia.home.push(s.sp.punt.home)
|
||||
s.sp.striscia.guest.push(" ")
|
||||
} else {
|
||||
s.sp.striscia.guest.push(s.sp.punt.guest)
|
||||
s.sp.striscia.home.push(" ")
|
||||
}
|
||||
|
||||
const cambioPalla = (team === "home" && !s.sp.servHome) || (team === "guest" && s.sp.servHome)
|
||||
if (cambioPalla) {
|
||||
s.sp.form[team].push(s.sp.form[team].shift())
|
||||
}
|
||||
|
||||
s.sp.servHome = team === "home"
|
||||
break
|
||||
}
|
||||
|
||||
case "decPunt": {
|
||||
if (s.sp.striscia.home.length > 1 && s.sp.storicoServizio.length > 0) {
|
||||
const tmpHome = s.sp.striscia.home.pop()
|
||||
s.sp.striscia.guest.pop()
|
||||
const statoServizio = s.sp.storicoServizio.pop()
|
||||
|
||||
if (tmpHome === " ") {
|
||||
s.sp.punt.guest--
|
||||
if (statoServizio.cambioPalla) {
|
||||
s.sp.form.guest.unshift(s.sp.form.guest.pop())
|
||||
}
|
||||
} else {
|
||||
s.sp.punt.home--
|
||||
if (statoServizio.cambioPalla) {
|
||||
s.sp.form.home.unshift(s.sp.form.home.pop())
|
||||
}
|
||||
}
|
||||
s.sp.servHome = statoServizio.servHome
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "incSet": {
|
||||
const team = action.team
|
||||
if (s.sp.set[team] === 2) {
|
||||
s.sp.set[team] = 0
|
||||
} else {
|
||||
s.sp.set[team]++
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "cambiaPalla": {
|
||||
if (s.sp.punt.home === 0 && s.sp.punt.guest === 0) {
|
||||
s.sp.servHome = !s.sp.servHome
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "resetta": {
|
||||
s.visuForm = false
|
||||
s.sp.punt.home = 0
|
||||
s.sp.punt.guest = 0
|
||||
s.sp.form = {
|
||||
home: ["1", "2", "3", "4", "5", "6"],
|
||||
guest: ["1", "2", "3", "4", "5", "6"],
|
||||
}
|
||||
s.sp.striscia = { home: [0], guest: [0] }
|
||||
s.sp.storicoServizio = []
|
||||
break
|
||||
}
|
||||
|
||||
case "toggleFormazione": {
|
||||
s.visuForm = !s.visuForm
|
||||
break
|
||||
}
|
||||
|
||||
case "toggleStriscia": {
|
||||
s.visuStriscia = !s.visuStriscia
|
||||
break
|
||||
}
|
||||
|
||||
case "toggleOrder": {
|
||||
s.order = !s.order
|
||||
break
|
||||
}
|
||||
|
||||
case "setNomi": {
|
||||
if (action.home !== undefined) s.sp.nomi.home = action.home
|
||||
if (action.guest !== undefined) s.sp.nomi.guest = action.guest
|
||||
break
|
||||
}
|
||||
|
||||
case "setModalita": {
|
||||
s.modalitaPartita = action.modalita
|
||||
break
|
||||
}
|
||||
|
||||
case "setFormazione": {
|
||||
if (action.team && action.form) {
|
||||
s.sp.form[action.team] = [...action.form]
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "confermaCambi": {
|
||||
const team = action.team
|
||||
const cambi = action.cambi || []
|
||||
const form = s.sp.form[team].map((val) => String(val).trim())
|
||||
const formAggiornata = [...form]
|
||||
|
||||
let valid = true
|
||||
for (const cambio of cambi) {
|
||||
const cin = (cambio.in || "").trim()
|
||||
const cout = (cambio.out || "").trim()
|
||||
if (!cin || !cout) continue
|
||||
if (!/^\d+$/.test(cin) || !/^\d+$/.test(cout)) { valid = false; break }
|
||||
if (cin === cout) { valid = false; break }
|
||||
if (formAggiornata.includes(cin)) { valid = false; break }
|
||||
if (!formAggiornata.includes(cout)) { valid = false; break }
|
||||
|
||||
const idx = formAggiornata.findIndex((val) => String(val).trim() === cout)
|
||||
if (idx !== -1) {
|
||||
formAggiornata.splice(idx, 1, cin)
|
||||
}
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
s.sp.form[team] = formAggiornata
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
13
src/main.js
13
src/main.js
@@ -1,10 +1,21 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import WaveUI from 'wave-ui'
|
||||
import 'wave-ui/dist/wave-ui.css'
|
||||
import DisplayPage from './components/DisplayPage.vue'
|
||||
import ControllerPage from './components/ControllerPage.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: DisplayPage },
|
||||
{ path: '/controller', component: ControllerPage },
|
||||
],
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(WaveUI)
|
||||
app.mount('#app')
|
||||
|
||||
140
src/style.css
140
src/style.css
@@ -58,6 +58,19 @@ button:focus-visible {
|
||||
padding-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.score-inline {
|
||||
display: inline-block;
|
||||
min-width: 3ch;
|
||||
text-align: center;
|
||||
}
|
||||
.serv-slot {
|
||||
display: inline-flex;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tal {
|
||||
text-align: left;
|
||||
}
|
||||
@@ -80,8 +93,23 @@ button:focus-visible {
|
||||
float: left;
|
||||
width: 50%;
|
||||
}
|
||||
.punteggio-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.punt {
|
||||
font-size: 60vh;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
min-height: 50vh;
|
||||
min-width: 50vw;
|
||||
max-width: 50vw;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form {
|
||||
font-size: 5vh;
|
||||
@@ -122,3 +150,115 @@ button:focus-visible {
|
||||
color: blue;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
base: process.env.NODE_ENV === 'production' ? '/segnap' : '/',
|
||||
base: '/',
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
|
||||
Reference in New Issue
Block a user