Compare commits
7 Commits
00a8300a75
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| bd90a2efe4 | |||
| 93372a1299 | |||
| 82c997d475 | |||
| 35c49247a8 | |||
| fec140df62 | |||
| c6eb493f25 | |||
| 7cf84a94d9 |
+2
-2
@@ -1,5 +1,5 @@
|
||||
# Stage 1: dipendenze condivise (build + dev)
|
||||
FROM node:20-alpine AS deps
|
||||
FROM node:24-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
@@ -16,7 +16,7 @@ COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Stage 4: runtime di produzione
|
||||
FROM node:20-alpine AS runtime
|
||||
FROM node:24-alpine AS runtime
|
||||
WORKDIR /app
|
||||
ENV NODE_ENV=production PORT=3000
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Segnapunti
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -9,8 +9,21 @@ Segnapunti digitale in tempo reale per partite di pallavolo. Un server centrale
|
||||
|
||||
---
|
||||
|
||||
## Requisiti
|
||||
|
||||
| Strumento | Versione minima | Uso |
|
||||
|---|---|---|
|
||||
| Node.js | >= 24 | Sviluppo locale (`npm run dev`) |
|
||||
| npm | >= 11 | Sviluppo locale |
|
||||
| Docker + Docker Compose | — | Deploy in produzione; alternativa containerizzata allo sviluppo locale |
|
||||
|
||||
Versione Node pinnata in `.nvmrc` e in `engines` di `package.json`. Con [nvm](https://github.com/nvm-sh/nvm): `nvm use`.
|
||||
|
||||
---
|
||||
|
||||
## Indice
|
||||
|
||||
- [Requisiti](#requisiti)
|
||||
- [Architettura](#architettura)
|
||||
- [Guida utente](#guida-utente)
|
||||
- [Funzionalità](#funzionalità)
|
||||
@@ -18,6 +31,7 @@ Segnapunti digitale in tempo reale per partite di pallavolo. Un server centrale
|
||||
- [Sviluppo](#sviluppo)
|
||||
- [Sviluppo con Docker](#sviluppo-con-docker)
|
||||
- [Test](#test)
|
||||
- [Changelog](#changelog)
|
||||
|
||||
---
|
||||
|
||||
@@ -166,7 +180,6 @@ La **partita** è vinta da chi raggiunge 3 set su 5 (modalità `3/5`) o 2 set su
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Deploy con Docker
|
||||
|
||||
### Prima installazione
|
||||
@@ -209,13 +222,6 @@ docker run --rm -p 3000:3000 -v ./.segnapunti:/app/.segnapunti segnapunti:local
|
||||
|
||||
## Sviluppo
|
||||
|
||||
### Requisiti
|
||||
|
||||
| Strumento | Versione minima |
|
||||
|---|---|
|
||||
| Node.js | >= 18 |
|
||||
| npm | >= 9 |
|
||||
|
||||
### Avvio
|
||||
|
||||
```bash
|
||||
@@ -253,7 +259,7 @@ docker compose -f docker-compose.dev.yml up --build
|
||||
| `http://localhost:5173/display` | Display |
|
||||
| `http://localhost:5173/controller` | Controller |
|
||||
|
||||
Le modifiche ai file sorgente sull'host si riflettono subito nel container (bind-mount); `node_modules` resta invece un volume anonimo del container, per non farsi sovrascrivere dal bind-mount e per evitare incompatibilità tra i binari nativi installati sull'host e quelli richiesti dal container (es. host ARM / container `node:20-alpine` su x86).
|
||||
Le modifiche ai file sorgente sull'host si riflettono subito nel container (bind-mount); `node_modules` resta invece un volume anonimo del container, per non farsi sovrascrivere dal bind-mount e per evitare incompatibilità tra i binari nativi installati sull'host e quelli richiesti dal container (es. host ARM / container `node:24-alpine` su x86).
|
||||
|
||||
Per fermare e rimuovere il container:
|
||||
|
||||
|
||||
Generated
+426
-382
File diff suppressed because it is too large
Load Diff
+5
-2
@@ -3,6 +3,9 @@
|
||||
"private": true,
|
||||
"version": "2.1.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "vite build",
|
||||
@@ -30,12 +33,12 @@
|
||||
"@playwright/test": "^1.58.2",
|
||||
"@types/node": "^25.2.3",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vitest/ui": "^4.0.18",
|
||||
"@vitest/ui": "^4.1.10",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"happy-dom": "^20.6.1",
|
||||
"jsdom": "^28.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vitest": "^4.0.18"
|
||||
"vitest": "^4.1.10"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
+23
-20
@@ -34,11 +34,11 @@ export function buildRefertoHtml(state, now = new Date()) {
|
||||
return `
|
||||
<div class="form-row">
|
||||
<div class="form-team">
|
||||
<div class="form-team-name">${nomi.home}</div>
|
||||
<div class="form-team-name form-team-name-h">${nomi.home}</div>
|
||||
<div class="giocatori">${formazioneSet.home.map(giocatoreHtml).join('')}</div>
|
||||
</div>
|
||||
<div class="form-team">
|
||||
<div class="form-team-name">${nomi.guest}</div>
|
||||
<div class="form-team-name form-team-name-g">${nomi.guest}</div>
|
||||
<div class="giocatori">${formazioneSet.guest.map(giocatoreHtml).join('')}</div>
|
||||
</div>
|
||||
</div>`
|
||||
@@ -79,7 +79,11 @@ export function buildRefertoHtml(state, now = new Date()) {
|
||||
|
||||
const vinc = vincitoreSet(s)
|
||||
const nomeVinc = vinc === 'h' ? nomi.home : vinc === 'g' ? nomi.guest : ''
|
||||
const etichettaVinc = nomeVinc ? ` · vinto da <strong>${nomeVinc}</strong>` : ''
|
||||
const etichettaVinc = nomeVinc ? `<span class="set-vinc">vinto da <strong>${nomeVinc}</strong></span>` : ''
|
||||
|
||||
const servIcon = '<img src="/serv-nero.png" class="serv-icon" alt="Servizio">'
|
||||
const servHomeIcon = s.serv === 'h' ? servIcon : ''
|
||||
const servGuestIcon = s.serv === 'g' ? servIcon : ''
|
||||
|
||||
const puntiHtml = punti.map(p =>
|
||||
`<span class="punto punto-${p.chi}">${p.h}-${p.g}</span>`
|
||||
@@ -90,7 +94,8 @@ export function buildRefertoHtml(state, now = new Date()) {
|
||||
<div class="set-barra"><span>SET</span><span class="set-numero">${i + 1}</span></div>
|
||||
<div class="set-corpo">
|
||||
<div class="set-header">
|
||||
${nomi.home} <strong>${h}</strong> · <strong>${g}</strong> ${nomi.guest}${etichettaVinc}
|
||||
<span>${servHomeIcon}${nomi.home} <strong>${h}</strong> · <strong>${g}</strong> ${nomi.guest}${servGuestIcon}</span>
|
||||
${etichettaVinc}
|
||||
</div>
|
||||
<div class="form-inizio">
|
||||
<div class="form-inizio-label">Formazione di partenza</div>
|
||||
@@ -130,25 +135,23 @@ export function buildRefertoHtml(state, now = new Date()) {
|
||||
.meta { font-size: 11px; color: #333; margin-top: 3px; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
|
||||
.squadre-row { display: flex; align-items: center; gap: 14px; }
|
||||
.badge-ab { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; border: 2px solid #111; font-weight: bold; font-size: 12px; flex-shrink: 0; }
|
||||
.badge-ab { display: inline-flex; align-items: center; justify-content: center; height: 22px; padding: 0 8px; border-radius: 11px; border: 2px solid #111; font-weight: bold; font-size: 12px; flex-shrink: 0; }
|
||||
.nome-sq { font-size: 18px; font-weight: bold; }
|
||||
.vs { font-size: 12px; color: #666; font-weight: bold; }
|
||||
|
||||
.risultato-riepilogo { display: flex; align-items: center; gap: 10px; }
|
||||
.risultato { font-size: 30px; font-weight: bold; letter-spacing: 3px; }
|
||||
.modalita-label { font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||
|
||||
.set-section { display: flex; margin-bottom: 10px; border: 2px solid #111; }
|
||||
.set-barra { flex: 0 0 26px; background: var(--teal); color: #fff; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; border-right: 2px solid #111; }
|
||||
.set-barra span:first-child { writing-mode: vertical-rl; transform: rotate(180deg); font-weight: bold; font-size: 11px; letter-spacing: 2px; }
|
||||
.set-numero { font-weight: bold; font-size: 15px; }
|
||||
.set-corpo { flex: 1; min-width: 0; }
|
||||
.set-header { background: #eaf7fb; padding: 6px 12px; font-size: 13px; border-bottom: 1px solid #111; }
|
||||
.set-header { background: #eaf7fb; padding: 6px 12px; font-size: 13px; border-bottom: 1px solid #111; display: flex; justify-content: space-between; align-items: center; gap: 10px; }
|
||||
.set-vinc { flex-shrink: 0; }
|
||||
.serv-icon { width: 12px; height: 12px; vertical-align: middle; margin: 0 3px; }
|
||||
|
||||
.punti-grid { display: flex; flex-wrap: wrap; gap: 0; padding: 0; border-top: 1px solid #111; }
|
||||
.punto { display: inline-flex; align-items: center; justify-content: center; min-width: 34px; padding: 3px 4px; font-size: 11px; font-family: 'Courier New', monospace; white-space: nowrap; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; }
|
||||
.punto-h { background: #d0e8ff; color: #003a6e; }
|
||||
.punto-g { background: #ffddc0; color: #6e2700; }
|
||||
.punto-h { background: #f5c518; color: #111; }
|
||||
.punto-g { background: #2196f3; color: #fff; }
|
||||
|
||||
.eventi-set { padding: 6px 12px; border-bottom: 1px solid #111; }
|
||||
.eventi-label { font-size: 10px; font-weight: bold; letter-spacing: 0.5px; text-transform: uppercase; color: #0f8faf; margin-bottom: 4px; }
|
||||
@@ -160,6 +163,8 @@ export function buildRefertoHtml(state, now = new Date()) {
|
||||
.form-row { display: flex; gap: 40px; }
|
||||
.form-team { flex: 1; }
|
||||
.form-team-name { font-weight: bold; font-size: 12px; margin-bottom: 5px; }
|
||||
.form-team-name-h { color: #f5c518; }
|
||||
.form-team-name-g { color: #2196f3; }
|
||||
.giocatori { display: flex; gap: 5px; flex-wrap: wrap; }
|
||||
.giocatore { background: #fff; border-radius: 50%; width: 26px; height: 26px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 11px; border: 2px solid #111; }
|
||||
|
||||
@@ -181,11 +186,11 @@ export function buildRefertoHtml(state, now = new Date()) {
|
||||
<div class="meta">${dataOra} · Modalità: ${modalitaPartita}</div>
|
||||
</div>
|
||||
<div class="squadre-row">
|
||||
<span class="badge-ab">A</span>
|
||||
<span class="badge-ab">Casa</span>
|
||||
<span class="nome-sq">${nomi.home}</span>
|
||||
<span class="vs">vs</span>
|
||||
<span class="nome-sq">${nomi.guest}</span>
|
||||
<span class="badge-ab">B</span>
|
||||
<span class="badge-ab">Ospiti</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,15 +200,11 @@ export function buildRefertoHtml(state, now = new Date()) {
|
||||
<div class="finale-box">
|
||||
<div class="finale-box-header">Risultato finale</div>
|
||||
<table class="finale-tabella">
|
||||
<thead><tr><th>Set</th><th>A</th><th>B</th></tr></thead>
|
||||
<thead><tr><th>Set</th><th>${nomi.home}</th><th>${nomi.guest}</th></tr></thead>
|
||||
<tbody>${risultatoSetHtml}</tbody>
|
||||
</table>
|
||||
<div class="finale-vince">Vince ${setVinti.home >= setVinti.guest ? nomi.home : nomi.guest} ${setVinti.home} – ${setVinti.guest}</div>
|
||||
</div>
|
||||
<div class="risultato-riepilogo">
|
||||
<div class="risultato">${setVinti.home} – ${setVinti.guest}</div>
|
||||
<div class="modalita-label">set vinti</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
@@ -219,5 +220,7 @@ export function generaReferto(state) {
|
||||
const w = window.open('', '_blank')
|
||||
w.document.write(html)
|
||||
w.document.close()
|
||||
w.print()
|
||||
// Aspetta il caricamento delle immagini (icona servizio) prima di stampare,
|
||||
// altrimenti la stampa parte con risorse ancora in caricamento e appaiono vuote.
|
||||
w.addEventListener('load', () => w.print())
|
||||
}
|
||||
|
||||
@@ -11,10 +11,12 @@ describe('generaReferto (referto.js)', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('apre una nuova scheda, scrive il referto e avvia la stampa', () => {
|
||||
it('apre una nuova scheda, scrive il referto e avvia la stampa dopo il caricamento', () => {
|
||||
const listeners = {}
|
||||
const popup = {
|
||||
document: { write: vi.fn(), close: vi.fn() },
|
||||
print: vi.fn(),
|
||||
addEventListener: vi.fn((evt, cb) => { listeners[evt] = cb }),
|
||||
}
|
||||
const openSpy = vi.spyOn(window, 'open').mockReturnValue(popup)
|
||||
|
||||
@@ -26,6 +28,9 @@ describe('generaReferto (referto.js)', () => {
|
||||
expect(popup.document.write).toHaveBeenCalledTimes(1)
|
||||
expect(popup.document.write.mock.calls[0][0]).toContain('Antoniana')
|
||||
expect(popup.document.close).toHaveBeenCalledTimes(1)
|
||||
expect(popup.print).not.toHaveBeenCalled()
|
||||
|
||||
listeners.load()
|
||||
expect(popup.print).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user