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" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@fontsource/bangers": "^5.1.1", "@fontsource/press-start-2p": "^5.2.5",
"@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/react-fontawesome": "^0.2.2",

12
client/pnpm-lock.yaml generated
View File

@ -8,9 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@fontsource/bangers': '@fontsource/press-start-2p':
specifier: ^5.1.1 specifier: ^5.2.5
version: 5.1.1 version: 5.2.5
'@fortawesome/fontawesome-svg-core': '@fortawesome/fontawesome-svg-core':
specifier: ^6.7.2 specifier: ^6.7.2
version: 6.7.2 version: 6.7.2
@ -327,8 +327,8 @@ packages:
resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@fontsource/bangers@5.1.1': '@fontsource/press-start-2p@5.2.5':
resolution: {integrity: sha512-+OzMvd6OXRipNWjGtcRTz1kVn+ML2A+4OsejqroZeUju74qncQ6lhlP3cZzu6hijXR8U9rm603lufzYdVAOdsA==} resolution: {integrity: sha512-MmGLqhkv0kuoyeGgGkquEMRxJP6auc6918bKd8uTWP2beXMWLZZwCfXCqmskFLf0XYbtbzxuRXLjTnQBeTwsMQ==}
'@fortawesome/fontawesome-common-types@6.7.2': '@fortawesome/fontawesome-common-types@6.7.2':
resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==}
@ -1786,7 +1786,7 @@ snapshots:
'@eslint/core': 0.12.0 '@eslint/core': 0.12.0
levn: 0.4.1 levn: 0.4.1
'@fontsource/bangers@5.1.1': {} '@fontsource/press-start-2p@5.2.5': {}
'@fortawesome/fontawesome-common-types@6.7.2': {} '@fortawesome/fontawesome-common-types@6.7.2': {}

View File

@ -17,55 +17,67 @@
min-width: 120px min-width: 120px
background-color: $primary background-color: $primary
color: #000 color: #000
box-shadow: // Enhanced pixel art style with jagged edges
4px 4px 0 #000, image-rendering: pixelated
inset -4px -4px 0 darken($primary, 30%), box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
inset 4px 4px 0 lighten($primary, 10%)
transition: all 0.1s ease 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 &:hover
transform: translate(-2px, -2px) transform: translate(-2px, -2px)
box-shadow: box-shadow: 6px 6px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
6px 6px 0 #000,
inset -4px -4px 0 darken($primary, 30%),
inset 4px 4px 0 lighten($primary, 10%)
&:active &:active
transform: translate(4px, 4px) transform: translate(4px, 4px)
box-shadow: box-shadow: 0 0 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
0 0 0 #000,
inset -4px -4px 0 darken($primary, 30%),
inset 4px 4px 0 lighten($primary, 10%)
&:disabled &:disabled
opacity: 0.5 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 // Button types
&.primary &.primary
background-color: $primary background-color: $primary
color: #000 color: #000
box-shadow: box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($primary, 30%), inset 4px 4px 0 lighten($primary, 10%)
4px 4px 0 #000,
inset -4px -4px 0 darken($primary, 30%),
inset 4px 4px 0 lighten($primary, 10%)
&.secondary &.secondary
background-color: $secondary background-color: $secondary
color: #000 color: #000
box-shadow: box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($secondary, 30%), inset 4px 4px 0 lighten($secondary, 10%)
4px 4px 0 #000,
inset -4px -4px 0 darken($secondary, 30%),
inset 4px 4px 0 lighten($secondary, 10%)
&.accent &.accent
background-color: $accent background-color: $accent
color: #000 color: #000
box-shadow: box-shadow: 4px 4px 0 #000, inset -4px -4px 0 darken($accent, 30%), inset 4px 4px 0 lighten($accent, 10%)
4px 4px 0 #000,
inset -4px -4px 0 darken($accent, 30%), @keyframes pixel-breathe
inset 4px 4px 0 lighten($accent, 10%) 0%, 100%
transform: scale(1)
50%
transform: scale(1.01)
// Primary button // Primary button
.btn.primary .btn.primary
@ -73,10 +85,10 @@
color: #000 color: #000
box-shadow: 0 4px 15px rgba($primary, 0.5) box-shadow: 0 4px 15px rgba($primary, 0.5)
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4)
&:hover &:hover
background: linear-gradient(45deg, $primary, lighten($primary, 10%)) background: linear-gradient(45deg, $primary, lighten($primary, 10%))
&:focus &:focus
box-shadow: 0 0 0 2px rgba($primary, 0.5) box-shadow: 0 0 0 2px rgba($primary, 0.5)
@ -86,10 +98,10 @@
color: #000 color: #000
box-shadow: 0 4px 15px rgba($secondary, 0.5) box-shadow: 0 4px 15px rgba($secondary, 0.5)
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4) text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4)
&:hover &:hover
background: linear-gradient(45deg, $secondary, lighten($secondary, 10%)) background: linear-gradient(45deg, $secondary, lighten($secondary, 10%))
&:focus &:focus
box-shadow: 0 0 0 2px rgba($secondary, 0.5) box-shadow: 0 0 0 2px rgba($secondary, 0.5)
@ -98,10 +110,10 @@
background: linear-gradient(45deg, darken($danger, 10%), $danger) background: linear-gradient(45deg, darken($danger, 10%), $danger)
color: #fff color: #fff
box-shadow: 0 4px 15px rgba($danger, 0.5) box-shadow: 0 4px 15px rgba($danger, 0.5)
&:hover &:hover
background: linear-gradient(45deg, $danger, lighten($danger, 10%)) background: linear-gradient(45deg, $danger, lighten($danger, 10%))
&:focus &:focus
box-shadow: 0 0 0 2px rgba($danger, 0.5) box-shadow: 0 0 0 2px rgba($danger, 0.5)
@ -113,7 +125,7 @@
padding: 0.75rem 2rem padding: 0.75rem 2rem
transform-style: preserve-3d transform-style: preserve-3d
perspective: 1000px perspective: 1000px
&:after &:after
content: '' content: ''
position: absolute position: absolute
@ -126,9 +138,9 @@
transform: translateZ(-10px) transform: translateZ(-10px)
z-index: -1 z-index: -1
transition: transform 0.3s transition: transform 0.3s
&:hover &:hover
transform: translateY(-5px) rotateX(10deg) transform: translateY(-5px) rotateX(10deg)
&:after &:after
transform: translateZ(-5px) transform: translateZ(-5px)

View File

@ -274,10 +274,95 @@
gap: 0.5rem gap: 0.5rem
font-family: 'Press Start 2P', monospace font-family: 'Press Start 2P', monospace
font-size: 0.8rem font-size: 0.8rem
margin-bottom: 1rem
span span
color: $primary color: $primary
font-weight: bold 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 for automatic advances
.bye-container .bye-container

View File

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

View File

@ -78,8 +78,12 @@ const HomeScreen = () => {
</div> </div>
)} )}
<button type="submit" className="btn primary"> <button type="submit" className="btn primary pixelated full-width">
{isCreateMode ? 'Create New Game' : 'Join Game'} {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> </button>
</form> </form>
</div> </div>

View File

@ -1,8 +1,8 @@
// VotingScreen.jsx // VotingScreen.jsx
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useGame } from '../context/GameContext'; import { useGame } from '../context/GameContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 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'; import YouTubeEmbed from './YouTubeEmbed';
function VotingScreen() { function VotingScreen() {
@ -10,10 +10,30 @@ function VotingScreen() {
const [hasVoted, setHasVoted] = useState(false); const [hasVoted, setHasVoted] = useState(false);
const [selectedSong, setSelectedSong] = useState(null); const [selectedSong, setSelectedSong] = useState(null);
const [countdown, setCountdown] = useState(null); const [countdown, setCountdown] = useState(null);
// Get current battle // Get current battle
const battle = lobby?.currentBattle || null; 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 // Check if player has already voted
useEffect(() => { useEffect(() => {
if (battle && battle.votes && currentPlayer) { if (battle && battle.votes && currentPlayer) {
@ -35,11 +55,11 @@ function VotingScreen() {
}; };
// Submit final vote // Submit final vote
const handleSubmitVote = () => { const handleSubmitVote = async () => {
if (!selectedSong || hasVoted) return; if (!selectedSong || hasVoted) return;
submitVote(selectedSong); await submitVote(selectedSong);
setHasVoted(true); // Setting hasVoted is now handled by the useEffect that checks votes
}; };
// Get YouTube video IDs from links // Get YouTube video IDs from links
@ -100,7 +120,13 @@ function VotingScreen() {
</div> </div>
<div className="voting-status"> <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>
</div> </div>
); );
@ -113,7 +139,8 @@ function VotingScreen() {
<div className="voting-screen"> <div className="voting-screen">
<header className="voting-header"> <header className="voting-header">
<h1> <h1>
<FontAwesomeIcon icon={faVoteYea} /> Song Battle! <FontAwesomeIcon icon={tournamentPhase === 'Finals' ? faCrown : tournamentPhase === 'Semi-Finals' ? faMedal : faVoteYea} />
{tournamentPhase}
</h1> </h1>
<div className="round-info"> <div className="round-info">
<span>Round {battle.round + 1}</span> <span>Round {battle.round + 1}</span>
@ -208,7 +235,7 @@ function VotingScreen() {
disabled={!selectedSong} disabled={!selectedSong}
> >
Cast Vote Cast Vote
</button> </button>
</div> </div>
)} )}
@ -217,6 +244,27 @@ function VotingScreen() {
<div className="votes-count"> <div className="votes-count">
<span>{battle.voteCount || 0}</span> of <span>{lobby?.players?.filter(p => p.isConnected).length || 0}</span> votes <span>{battle.voteCount || 0}</span> of <span>{lobby?.players?.filter(p => p.isConnected).length || 0}</span> votes
</div> </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>
</div> </div>
); );

View File

@ -539,8 +539,15 @@ class GameManager {
return { error: 'Invalid song ID' }; return { error: 'Invalid song ID' };
} }
// Record the vote // Find player name for display purposes
lobby.currentBattle.votes.set(playerId, songId); 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 // Update vote counts
if (songId === lobby.currentBattle.song1.id) { if (songId === lobby.currentBattle.song1.id) {
@ -549,6 +556,9 @@ class GameManager {
lobby.currentBattle.song2Votes++; 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 // Check if all connected players have voted
const connectedPlayers = lobby.players.filter(p => p.isConnected).length; const connectedPlayers = lobby.players.filter(p => p.isConnected).length;
const voteCount = lobby.currentBattle.votes.size; const voteCount = lobby.currentBattle.votes.size;