diff --git a/client/package.json b/client/package.json index 0d2dd23..16fe4bb 100644 --- a/client/package.json +++ b/client/package.json @@ -10,7 +10,7 @@ "preview": "vite preview" }, "dependencies": { - "@fontsource/bangers": "^5.1.1", + "@fontsource/press-start-2p": "^5.2.5", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 89f06e7..8429c77 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@fontsource/bangers': - specifier: ^5.1.1 - version: 5.1.1 + '@fontsource/press-start-2p': + specifier: ^5.2.5 + version: 5.2.5 '@fortawesome/fontawesome-svg-core': specifier: ^6.7.2 version: 6.7.2 @@ -327,8 +327,8 @@ packages: resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@fontsource/bangers@5.1.1': - resolution: {integrity: sha512-+OzMvd6OXRipNWjGtcRTz1kVn+ML2A+4OsejqroZeUju74qncQ6lhlP3cZzu6hijXR8U9rm603lufzYdVAOdsA==} + '@fontsource/press-start-2p@5.2.5': + resolution: {integrity: sha512-MmGLqhkv0kuoyeGgGkquEMRxJP6auc6918bKd8uTWP2beXMWLZZwCfXCqmskFLf0XYbtbzxuRXLjTnQBeTwsMQ==} '@fortawesome/fontawesome-common-types@6.7.2': resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} @@ -1786,7 +1786,7 @@ snapshots: '@eslint/core': 0.12.0 levn: 0.4.1 - '@fontsource/bangers@5.1.1': {} + '@fontsource/press-start-2p@5.2.5': {} '@fortawesome/fontawesome-common-types@6.7.2': {} diff --git a/client/src/common/styles/buttons.sass b/client/src/common/styles/buttons.sass index 3923174..0bc85fb 100644 --- a/client/src/common/styles/buttons.sass +++ b/client/src/common/styles/buttons.sass @@ -17,55 +17,67 @@ min-width: 120px background-color: $primary color: #000 - box-shadow: - 4px 4px 0 #000, - inset -4px -4px 0 darken($primary, 30%), - inset 4px 4px 0 lighten($primary, 10%) + // Enhanced pixel art style with jagged edges + image-rendering: pixelated + box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%) transition: all 0.1s ease - + + // Add decorative pixel corners + &:before, &:after + content: '' + position: absolute + width: 4px + height: 4px + background-color: #000 + z-index: 2 + + &:before + top: -4px + left: -4px + + &:after + bottom: -4px + right: -4px + &:hover transform: translate(-2px, -2px) - box-shadow: - 6px 6px 0 #000, - inset -4px -4px 0 darken($primary, 30%), - inset 4px 4px 0 lighten($primary, 10%) - + box-shadow: 6px 6px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%) + &:active transform: translate(4px, 4px) - box-shadow: - 0 0 0 #000, - inset -4px -4px 0 darken($primary, 30%), - inset 4px 4px 0 lighten($primary, 10%) - + box-shadow: 0 0 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%) + &:disabled opacity: 0.5 - cursor: not-allowed - transform: none - + + // Full width modifier + &.full-width + width: 100% + margin-top: 1rem + padding: 1rem + font-size: 1rem + // Button types &.primary background-color: $primary color: #000 - box-shadow: - 4px 4px 0 #000, - inset -4px -4px 0 darken($primary, 30%), - inset 4px 4px 0 lighten($primary, 10%) - + box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%) + &.secondary background-color: $secondary color: #000 - box-shadow: - 4px 4px 0 #000, - inset -4px -4px 0 darken($secondary, 30%), - inset 4px 4px 0 lighten($secondary, 10%) - + box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($secondary, 30%), inset 4px 4px 0 lighten($secondary, 10%) + &.accent background-color: $accent color: #000 - box-shadow: - 4px 4px 0 #000, - inset -4px -4px 0 darken($accent, 30%), - inset 4px 4px 0 lighten($accent, 10%) + box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($accent, 30%), inset 4px 4px 0 lighten($accent, 10%) + +@keyframes pixel-breathe + 0%, 100% + transform: scale(1) + 50% + transform: scale(1.01) // Primary button .btn.primary @@ -73,10 +85,10 @@ color: #000 box-shadow: 0 4px 15px rgba($primary, 0.5) text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) - + &:hover background: linear-gradient(45deg, $primary, lighten($primary, 10%)) - + &:focus box-shadow: 0 0 0 2px rgba($primary, 0.5) @@ -86,10 +98,10 @@ color: #000 box-shadow: 0 4px 15px rgba($secondary, 0.5) text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) - + &:hover background: linear-gradient(45deg, $secondary, lighten($secondary, 10%)) - + &:focus box-shadow: 0 0 0 2px rgba($secondary, 0.5) @@ -98,10 +110,10 @@ background: linear-gradient(45deg, darken($danger, 10%), $danger) color: #fff box-shadow: 0 4px 15px rgba($danger, 0.5) - + &:hover background: linear-gradient(45deg, $danger, lighten($danger, 10%)) - + &:focus box-shadow: 0 0 0 2px rgba($danger, 0.5) @@ -113,7 +125,7 @@ padding: 0.75rem 2rem transform-style: preserve-3d perspective: 1000px - + &:after content: '' position: absolute @@ -126,9 +138,9 @@ transform: translateZ(-10px) z-index: -1 transition: transform 0.3s - + &:hover transform: translateY(-5px) rotateX(10deg) - + &:after - transform: translateZ(-5px) + transform: translateZ(-5px) \ No newline at end of file diff --git a/client/src/common/styles/components/voting-screen.sass b/client/src/common/styles/components/voting-screen.sass index 030068e..6666d9d 100644 --- a/client/src/common/styles/components/voting-screen.sass +++ b/client/src/common/styles/components/voting-screen.sass @@ -274,10 +274,95 @@ gap: 0.5rem font-family: 'Press Start 2P', monospace font-size: 0.8rem + margin-bottom: 1rem span color: $primary font-weight: bold + + // Player votes list styling + .player-votes + background-color: rgba(0, 0, 0, 0.2) + border: 2px dashed rgba(255, 255, 255, 0.2) + border-radius: 0.5rem + padding: 0.75rem + margin-top: 0.5rem + + h4 + margin: 0 0 0.75rem 0 + font-family: 'Press Start 2P', monospace + font-size: 0.8rem + color: $secondary + position: relative + display: inline-block + + &:before, &:after + content: '>' + position: absolute + color: $accent + animation: pixel-blink 1s infinite + + &:before + left: -1rem + + &:after + right: -1rem + + .players-voted-list + list-style: none + padding: 0 + margin: 0 + display: grid + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)) + gap: 0.5rem + + li + padding: 0.5rem + border: 2px solid transparent + font-size: 0.75rem + display: flex + align-items: center + justify-content: space-between + transition: all 0.2s ease + position: relative + overflow: hidden + + &.voted + background-color: rgba($success, 0.1) + border-color: rgba($success, 0.5) + + .vote-icon + color: $success + margin-left: 0.5rem + filter: drop-shadow(0 0 2px rgba($success, 0.8)) + animation: pixel-pulse 1.5s infinite + + &:before + content: '' + position: absolute + top: 0 + left: 0 + width: 100% + height: 100% + background: repeating-linear-gradient( + 45deg, + transparent, + transparent 5px, + rgba($success, 0.1) 5px, + rgba($success, 0.1) 10px + ) + z-index: -1 + + &.not-voted + background-color: rgba(255, 255, 255, 0.05) + border-color: rgba(255, 255, 255, 0.1) + opacity: 0.7 + +@keyframes pixel-blink + 0%, 100% + opacity: 1 + 50% + opacity: 0.3 // Bye container for automatic advances .bye-container diff --git a/client/src/common/styles/main.sass b/client/src/common/styles/main.sass index 3eefe6c..46f402b 100644 --- a/client/src/common/styles/main.sass +++ b/client/src/common/styles/main.sass @@ -1,6 +1,7 @@ // Main styles for the Song Battle application @import './colors' @import './forms' +@import './buttons' // Component styles @import './components/home-screen' diff --git a/client/src/components/HomeScreen.jsx b/client/src/components/HomeScreen.jsx index f40bcbd..e62e61b 100644 --- a/client/src/components/HomeScreen.jsx +++ b/client/src/components/HomeScreen.jsx @@ -78,8 +78,12 @@ const HomeScreen = () => { )} - diff --git a/client/src/components/VotingScreen.jsx b/client/src/components/VotingScreen.jsx index a827dcd..5e9f913 100644 --- a/client/src/components/VotingScreen.jsx +++ b/client/src/components/VotingScreen.jsx @@ -1,8 +1,8 @@ // VotingScreen.jsx -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useGame } from '../context/GameContext'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faVoteYea, faTrophy, faMusic, faCheck } from '@fortawesome/free-solid-svg-icons'; +import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown } from '@fortawesome/free-solid-svg-icons'; import YouTubeEmbed from './YouTubeEmbed'; function VotingScreen() { @@ -10,10 +10,30 @@ function VotingScreen() { const [hasVoted, setHasVoted] = useState(false); const [selectedSong, setSelectedSong] = useState(null); const [countdown, setCountdown] = useState(null); - + // Get current battle const battle = lobby?.currentBattle || null; + // Calculate the tournament phase based on the round number and total songs + const tournamentPhase = useMemo(() => { + if (!lobby || !battle) return ''; + + // Get total number of songs in the tournament + const totalSongs = lobby.songs?.length || 0; + + if (totalSongs === 0) return 'Preliminaries'; + + // Calculate total rounds needed for the tournament + const totalRounds = Math.ceil(Math.log2(totalSongs)); + const currentRound = battle.round + 1; + const roundsRemaining = totalRounds - currentRound; + + if (roundsRemaining === 0) return 'Finals'; + if (roundsRemaining === 1) return 'Semi-Finals'; + if (roundsRemaining === 2) return 'Quarter-Finals'; + return 'Contestant Round'; + }, [lobby, battle]); + // Check if player has already voted useEffect(() => { if (battle && battle.votes && currentPlayer) { @@ -35,11 +55,11 @@ function VotingScreen() { }; // Submit final vote - const handleSubmitVote = () => { + const handleSubmitVote = async () => { if (!selectedSong || hasVoted) return; - submitVote(selectedSong); - setHasVoted(true); + await submitVote(selectedSong); + // Setting hasVoted is now handled by the useEffect that checks votes }; // Get YouTube video IDs from links @@ -100,7 +120,13 @@ function VotingScreen() {
- +
); @@ -113,7 +139,8 @@ function VotingScreen() {

- Song Battle! + + {tournamentPhase}

Round {battle.round + 1} @@ -208,7 +235,7 @@ function VotingScreen() { disabled={!selectedSong} > Cast Vote - +
)} @@ -217,6 +244,27 @@ function VotingScreen() {
{battle.voteCount || 0} of {lobby?.players?.filter(p => p.isConnected).length || 0} votes
+ + {/* Player voting status list */} +
+

Voters

+
    + {lobby?.players?.filter(p => p.isConnected).map(player => { + // Check if this player has voted + const hasPlayerVoted = battle.votes && + Object.keys(battle.votes).includes(player.id); + + return ( +
  • + {player.name} {player.id === currentPlayer.id && '(You)'} + {hasPlayerVoted && + + } +
  • + ); + })} +
+
); diff --git a/server/game.js b/server/game.js index 425eed0..ddf9b66 100644 --- a/server/game.js +++ b/server/game.js @@ -539,8 +539,15 @@ class GameManager { return { error: 'Invalid song ID' }; } - // Record the vote - lobby.currentBattle.votes.set(playerId, songId); + // Find player name for display purposes + const player = lobby.players.find(p => p.id === playerId); + const playerName = player ? player.name : 'Unknown Player'; + + // Record the vote with player name for UI display + lobby.currentBattle.votes.set(playerId, { + songId, + playerName + }); // Update vote counts if (songId === lobby.currentBattle.song1.id) { @@ -549,6 +556,9 @@ class GameManager { lobby.currentBattle.song2Votes++; } + // Add a voteCount attribute for easier UI rendering + lobby.currentBattle.voteCount = lobby.currentBattle.votes.size; + // Check if all connected players have voted const connectedPlayers = lobby.players.filter(p => p.isConnected).length; const voteCount = lobby.currentBattle.votes.size;