Enhance Voting and Home screens with pixel art styles; add player voting status list and update button designs for improved UI experience

This commit is contained in:
Mathias Wagner 2025-04-24 17:56:53 +02:00
parent 38ed69bf5b
commit fca6baa694
8 changed files with 221 additions and 61 deletions

View File

@ -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",

12
client/pnpm-lock.yaml generated
View File

@ -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': {}

View File

@ -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)

View File

@ -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

View File

@ -1,6 +1,7 @@
// Main styles for the Song Battle application
@import './colors'
@import './forms'
@import './buttons'
// Component styles
@import './components/home-screen'

View File

@ -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>

View File

@ -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>
);

View File

@ -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;