From fca6baa6949c2eaad7d0ae4084f999640fc9daf0 Mon Sep 17 00:00:00 2001
From: Mathias Wagner <germannewsmaker@gmail.com>
Date: Thu, 24 Apr 2025 17:56:53 +0200
Subject: [PATCH] Enhance Voting and Home screens with pixel art styles; add
 player voting status list and update button designs for improved UI
 experience

---
 client/package.json                           |  2 +-
 client/pnpm-lock.yaml                         | 12 +--
 client/src/common/styles/buttons.sass         | 96 +++++++++++--------
 .../styles/components/voting-screen.sass      | 85 ++++++++++++++++
 client/src/common/styles/main.sass            |  1 +
 client/src/components/HomeScreen.jsx          |  6 +-
 client/src/components/VotingScreen.jsx        | 66 +++++++++++--
 server/game.js                                | 14 ++-
 8 files changed, 221 insertions(+), 61 deletions(-)

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 = () => {
             </div>
           )}
           
-          <button type="submit" className="btn primary">
+          <button type="submit" className="btn primary pixelated full-width">
             {isCreateMode ? 'Create New Game' : 'Join Game'}
+            <span className="pixel-corner tl"></span>
+            <span className="pixel-corner tr"></span>
+            <span className="pixel-corner bl"></span>
+            <span className="pixel-corner br"></span>
           </button>
         </form>
       </div>
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() {
         </div>
         
         <div className="voting-status">
-          <button className="btn primary" onClick={() => submitVote(battle.song1.id)}>Continue to Next Battle</button>
+          <button className="btn primary pixelated full-width" onClick={() => submitVote(battle.song1.id)}>
+            Continue to Next Battle
+            <span className="pixel-corner tl"></span>
+            <span className="pixel-corner tr"></span>
+            <span className="pixel-corner bl"></span>
+            <span className="pixel-corner br"></span>
+          </button>
         </div>
       </div>
     );
@@ -113,7 +139,8 @@ function VotingScreen() {
     <div className="voting-screen">
       <header className="voting-header">
         <h1>
-          <FontAwesomeIcon icon={faVoteYea} /> Song Battle!
+          <FontAwesomeIcon icon={tournamentPhase === 'Finals' ? faCrown : tournamentPhase === 'Semi-Finals' ? faMedal : faVoteYea} /> 
+          {tournamentPhase}
         </h1>
         <div className="round-info">
           <span>Round {battle.round + 1}</span>
@@ -208,7 +235,7 @@ function VotingScreen() {
             disabled={!selectedSong}
           >
             Cast Vote
-          </button>
+                      </button>
         </div>
       )}
       
@@ -217,6 +244,27 @@ function VotingScreen() {
         <div className="votes-count">
           <span>{battle.voteCount || 0}</span> of <span>{lobby?.players?.filter(p => p.isConnected).length || 0}</span> votes
         </div>
+        
+        {/* Player voting status list */}
+        <div className="player-votes">
+          <h4>Voters</h4>
+          <ul className="players-voted-list">
+            {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 (
+                <li key={player.id} className={hasPlayerVoted ? 'voted' : 'not-voted'}>
+                  {player.name} {player.id === currentPlayer.id && '(You)'}
+                  {hasPlayerVoted && 
+                    <FontAwesomeIcon icon={faCheck} className="vote-icon" />
+                  }
+                </li>
+              );
+            })}
+          </ul>
+        </div>
       </div>
     </div>
   );
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;