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
All checks were successful
Deploy Feature / deploy-feature (push) Successful in 27s
This commit is contained in:
62
src/App.vue
62
src/App.vue
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user