Make viewers unabler to join before teams are finalized. Viewers are on teams
All checks were successful
Deploy Feature / deploy-feature (push) Successful in 27s

This commit is contained in:
Johnny322
2026-02-24 22:15:57 +01:00
parent 7c12d25367
commit 993b1a2c13
2 changed files with 79 additions and 4 deletions

View File

@@ -132,12 +132,24 @@
<section v-if="step === 'game'" class="game-layout">
<aside class="scoreboard">
<h2>Teams</h2>
<div v-if="!canControlGame" class="viewer-team-select">
<label for="viewer-team">Playing For</label>
<select id="viewer-team" v-model="viewerTeamId" class="input">
<option value="">Select team</option>
<option v-for="team in teams" :key="`viewer-${team.id}`" :value="team.id">
{{ team.name }}
</option>
</select>
</div>
<div class="team-list">
<button
v-for="team in teams"
:key="team.id"
class="team-score-card"
:class="{ active: team.id === currentSelectorId }"
:class="{
active: team.id === currentSelectorId,
viewerTeam: !canControlGame && !!viewerTeamId && team.id === viewerTeamId
}"
:style="teamGradient(team)"
@click="awardPoints(team.id)"
:disabled="!canAward || !canControlGame"
@@ -251,6 +263,9 @@
@pause="handlePlayerPause"
@ended="handlePlayerPause"
></audio>
<button v-if="showEnableAudio" class="primary enable-audio" @click="enableViewerAudio">
Tap To Enable Audio
</button>
<div v-if="!currentClipUrl" class="player-empty"></div>
</div>
</div>
@@ -380,7 +395,10 @@ export default {
isApplyingRemote: false,
queuedRemoteState: null as RealtimeState | null,
syncTimer: 0,
playbackSyncInterval: 0
playbackSyncInterval: 0,
audioUnlocked: true,
latestRemoteState: null as RealtimeState | null,
viewerTeamId: ''
}
},
async mounted() {
@@ -412,6 +430,9 @@ export default {
canProceedToSelect() {
return this.canProceed && this.canControlGame && !!this.normalizedGameIdInput && !this.sessionBusy
},
showEnableAudio() {
return !this.canControlGame && !!this.currentClipUrl && !this.audioUnlocked
},
canControlGame() {
return !this.gameId || this.isHost
},
@@ -497,6 +518,7 @@ export default {
this.gameId = gameId
this.gameIdInput = gameId
this.isHost = true
this.audioUnlocked = true
this.setGameInUrl(gameId)
await this.connectSession()
} catch (error) {
@@ -514,9 +536,15 @@ export default {
try {
const existsResponse = await fetch(`${REALTIME_BASE}/sessions/${normalized}`)
if (!existsResponse.ok) throw new Error('Session not found')
const sessionInfo = (await existsResponse.json()) as { hasState?: boolean }
if (!sessionInfo.hasState) {
throw new Error('Game is not open yet. Host must press Next: Choose Game first.')
}
this.gameId = normalized
this.gameIdInput = normalized
this.isHost = false
this.audioUnlocked = false
this.viewerTeamId = ''
this.setGameInUrl(normalized)
await this.connectSession()
} catch (error) {
@@ -531,6 +559,9 @@ export default {
this.gameIdInput = ''
this.isHost = false
this.syncError = ''
this.audioUnlocked = true
this.latestRemoteState = null
this.viewerTeamId = ''
this.stopHostPlaybackSync()
this.setGameInUrl('')
},
@@ -598,6 +629,7 @@ export default {
publishState() {
if (!this.isHost || !this.socketConnected || !this.socket) return
if (this.isApplyingRemote) return
if (this.step === 'setup') return
const payload = {
type: 'state:update',
state: this.buildRealtimeState()
@@ -618,6 +650,7 @@ export default {
this.queuedRemoteState = state
return
}
this.latestRemoteState = state
const previousClipUrl = this.currentClipUrl
this.isApplyingRemote = true
try {
@@ -633,12 +666,24 @@ export default {
this.currentSelectorId = state.currentSelectorId
this.lastAwardedTeamId = state.lastAwardedTeamId
this.isAnswerClip = state.isAnswerClip
if (this.viewerTeamId && !this.teams.some((team) => team.id === this.viewerTeamId)) {
this.viewerTeamId = ''
}
} finally {
this.isApplyingRemote = false
}
await nextTick()
this.syncRemotePlayback(previousClipUrl !== this.currentClipUrl, state)
},
async enableViewerAudio() {
if (this.canControlGame) return
this.audioUnlocked = true
this.ensureAudioContext()
await nextTick()
if (this.latestRemoteState) {
this.syncRemotePlayback(false, this.latestRemoteState)
}
},
getCurrentTileStatus() {
if (!this.currentTileKey) return 'available'
return this.tiles[this.currentTileKey]?.status || 'available'
@@ -691,6 +736,7 @@ export default {
}
if (tileStatus === 'playing' || tileStatus === 'guessed') {
if (!this.audioUnlocked) return
this.ensureAudioContext()
player.play().catch(() => {})
}
@@ -709,7 +755,11 @@ export default {
setupAudioGraph() {
const player = this.getPlayer()
if (!player) return
this.ensureAudioContext()
try {
this.ensureAudioContext()
} catch {
return
}
if (this.analyser) {
this.analyser.disconnect()
@@ -723,7 +773,11 @@ export default {
if (this.mediaSource) {
this.mediaSource.disconnect()
}
this.mediaSource = context.createMediaElementSource(player)
try {
this.mediaSource = context.createMediaElementSource(player)
} catch {
return
}
this.mediaElement = player
}

View File

@@ -282,6 +282,17 @@ button {
gap: 12px;
}
.viewer-team-select {
margin-bottom: 12px;
}
.viewer-team-select label {
display: block;
margin-bottom: 6px;
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.8);
}
.team-score-card {
width: 100%;
border: none;
@@ -296,6 +307,10 @@ button {
outline: 2px solid #ffffff;
}
.team-score-card.viewerTeam {
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.6) inset;
}
.team-name {
font-weight: 600;
}
@@ -539,6 +554,12 @@ audio.hidden-audio {
color: rgba(255, 255, 255, 0.6);
}
.enable-audio {
position: absolute;
bottom: 20px;
z-index: 3;
}
.end-panel {
text-align: center;
}