diff --git a/src/App.vue b/src/App.vue index 5e8a3f9..230fcaa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,5 +1,5 @@ @@ -334,6 +339,7 @@ type RealtimeState = { currentSelectorId: string | null lastAwardedTeamId: string | null isAnswerClip: boolean + guessingTeamId: string | null } type RealtimeMessage = { @@ -399,7 +405,8 @@ export default { syncTimer: 0, audioUnlocked: true, latestRemoteState: null as RealtimeState | null, - viewerTeamId: '' + viewerTeamId: '', + guessingTeamId: null as string | null } }, async mounted() { @@ -437,14 +444,26 @@ export default { canViewerGuess() { return ( !this.canControlGame && - this.audioUnlocked && !!this.viewerTeamId && this.getCurrentTileStatus() === 'playing' ) }, + viewerGuessVisible() { + return !this.canControlGame && this.getCurrentTileStatus() === 'playing' + }, canControlGame() { return !this.gameId || this.isHost }, + isGuessSuspended() { + return this.step === 'game' && this.getCurrentTileStatus() === 'paused' + }, + guessingTeamLabel() { + if (this.guessingTeamId) { + const team = this.teams.find((candidate) => candidate.id === this.guessingTeamId) + if (team?.name?.trim()) return team.name.trim() + } + return 'A team' + }, currentSelector() { return this.teams.find((team) => team.id === this.currentSelectorId) || null }, @@ -571,6 +590,7 @@ export default { this.audioUnlocked = true this.latestRemoteState = null this.viewerTeamId = '' + this.guessingTeamId = null this.setGameInUrl('') }, async connectSession() { @@ -632,7 +652,8 @@ export default { currentClipUrl: this.currentClipUrl, currentSelectorId: this.currentSelectorId, lastAwardedTeamId: this.lastAwardedTeamId, - isAnswerClip: this.isAnswerClip + isAnswerClip: this.isAnswerClip, + guessingTeamId: this.guessingTeamId } }, publishState() { @@ -675,6 +696,7 @@ export default { this.currentSelectorId = state.currentSelectorId this.lastAwardedTeamId = state.lastAwardedTeamId this.isAnswerClip = state.isAnswerClip + this.guessingTeamId = state.guessingTeamId || null if (this.viewerTeamId && !this.teams.some((team) => team.id === this.viewerTeamId)) { this.viewerTeamId = '' } @@ -710,27 +732,12 @@ export default { const status = this.getCurrentTileStatus() if (status !== 'playing') return const key = this.currentTileKey - const [cIndex, qIndex] = key.split('-').map(Number) - const clue = this.selectedGame.categories[cIndex].clues[qIndex] - this.tiles[key].status = 'guessed' + this.tiles[key].status = 'paused' this.lastAwardedTeamId = null - if (clue.answer) { - this.currentClipUrl = encodeURI(clue.answer) - this.isAnswerClip = true - await nextTick() - const player = this.getPlayer() - if (player) { - this.ensureAudioContext() - player.currentTime = 0 - player.load() - player.play().catch(() => {}) - } - } else { - const player = this.getPlayer() - player?.pause() - this.currentClipUrl = '' - this.isAnswerClip = false - } + this.guessingTeamId = teamId + this.isAnswerClip = false + const player = this.getPlayer() + player?.pause() this.queueStateSync() }, getCurrentTileStatus() { @@ -956,6 +963,7 @@ export default { this.currentTileKey = null this.currentClipUrl = '' this.lastAwardedTeamId = null + this.guessingTeamId = null this.teams = this.teams.map((team) => ({ ...team, score: 0 })) const randomTeam = this.teams[Math.floor(Math.random() * this.teams.length)] this.currentSelectorId = randomTeam?.id || null @@ -971,6 +979,7 @@ export default { this.currentClipUrl = '' this.lastAwardedTeamId = null this.isAnswerClip = false + this.guessingTeamId = null this.teardownAudio() this.queueStateSync() }, @@ -1018,6 +1027,7 @@ export default { this.currentTileKey = key this.currentClipUrl = encodeURI(clue.song) this.isAnswerClip = false + this.guessingTeamId = null await nextTick() const player = this.getPlayer() if (player) { @@ -1032,6 +1042,7 @@ export default { if (status === 'playing') { this.tiles[key].status = 'paused' + this.guessingTeamId = this.currentSelectorId const player = this.getPlayer() player?.pause() this.queueStateSync() @@ -1065,6 +1076,7 @@ export default { this.currentTileKey = null this.currentClipUrl = '' this.lastAwardedTeamId = null + this.guessingTeamId = null this.checkEnd() this.queueStateSync() } @@ -1077,6 +1089,7 @@ export default { if (status === 'paused') { this.tiles[key].status = 'playing' + this.guessingTeamId = null const player = this.getPlayer() player?.play().catch(() => {}) this.queueStateSync() @@ -1088,6 +1101,7 @@ export default { this.currentTileKey = null this.currentClipUrl = '' this.isAnswerClip = false + this.guessingTeamId = null const player = this.getPlayer() player?.pause() this.teardownAudio() @@ -1122,6 +1136,7 @@ export default { const finished = allTiles.every((status) => status === 'won' || status === 'void') if (finished) { this.step = 'end' + this.guessingTeamId = null } } }, diff --git a/src/styles.css b/src/styles.css index ff0446a..269163a 100644 --- a/src/styles.css +++ b/src/styles.css @@ -29,6 +29,42 @@ code { gap: 24px; } +.app-header, +.app-main { + transition: filter 0.25s ease, opacity 0.25s ease; +} + +.app.suspended .app-header, +.app.suspended .app-main { + filter: grayscale(1) brightness(0.55); + opacity: 0.75; +} + +.guess-overlay { + position: fixed; + inset: 0; + display: grid; + place-items: center; + background: rgba(8, 12, 26, 0.35); + backdrop-filter: blur(2px) grayscale(0.15); + pointer-events: none; + z-index: 12; +} + +.guess-overlay p { + margin: 0; + padding: 16px 24px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.3); + background: rgba(13, 17, 35, 0.88); + color: #ffffff; + font-family: 'Bebas Neue', sans-serif; + font-size: clamp(2.1rem, 4vw, 3.3rem); + letter-spacing: 0.07em; + text-transform: uppercase; + text-align: center; +} + .app-header { display: flex; justify-content: space-between; @@ -554,16 +590,33 @@ audio.hidden-audio { color: rgba(255, 255, 255, 0.6); } -.enable-audio { +.viewer-actions { position: absolute; + left: 50%; bottom: 20px; - z-index: 3; + transform: translateX(-50%); + z-index: 4; + display: flex; + align-items: center; + gap: 12px; +} + +.enable-audio { + border: 1px solid rgba(255, 255, 255, 0.4); } .viewer-guess { - position: absolute; - bottom: 20px; - z-index: 3; + font-size: 1rem; + font-weight: 800; + letter-spacing: 0.03em; + padding: 14px 28px; + background: linear-gradient(135deg, #ffce3a, #ff6a3a); + box-shadow: 0 12px 30px rgba(255, 106, 58, 0.35); +} + +.viewer-guess:disabled { + background: linear-gradient(135deg, #8f8f8f, #666666); + box-shadow: none; } .end-panel { @@ -606,4 +659,13 @@ audio.hidden-audio { .session-row { flex-wrap: wrap; } + + .viewer-actions { + flex-direction: column; + width: calc(100% - 24px); + } + + .viewer-actions .primary { + width: 100%; + } }