Separa app in client-server con WebSocket

- Aggiunto server Express + WebSocket (server.js)
- Creata pagina Display (solo visualizzazione punteggio)
- Creata pagina Controller (pannello comandi da mobile)
- Aggiunto Vue Router con rotte / e /controller
- Estratta logica di gioco condivisa in gameState.js
This commit is contained in:
2026-02-10 00:42:48 +01:00
parent 3789f25d0d
commit a40fad7194
9 changed files with 2280 additions and 185 deletions

View 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>