Add player name visibility toggle and enhance display logic in lobby and results screens
Some checks failed
Publish Docker image / Push Docker image to Docker Hub (push) Failing after 3m43s

This commit is contained in:
Mathias Wagner 2025-05-14 21:10:26 +02:00
parent 301e08b6e6
commit 0c543a1a01
10 changed files with 178 additions and 19 deletions

View File

@ -179,13 +179,27 @@
padding: 2rem padding: 2rem
width: 90% width: 90%
max-width: 500px max-width: 500px
border: 6px solid #000
box-shadow: 10px 10px 0 rgba(0, 0, 0, 0.5)
animation: modal-appear 0.3s ease-out
h2 h2
margin-top: 0 margin-top: 0
color: $primary color: $primary
font-family: 'Press Start 2P', monospace
font-size: 1.4rem
margin-bottom: 1.5rem
.modal-actions .modal-actions
display: flex display: flex
justify-content: flex-end justify-content: flex-end
gap: 1rem gap: 1rem
margin-top: 2rem margin-top: 2rem
@keyframes modal-appear
0%
transform: scale(0.8)
opacity: 0
100%
transform: scale(1)
opacity: 1

View File

@ -243,6 +243,14 @@
padding: 0.25rem 0.5rem padding: 0.25rem 0.5rem
border-radius: 1rem border-radius: 1rem
background-color: rgba(255, 255, 255, 0.1) background-color: rgba(255, 255, 255, 0.1)
margin-right: 0.5rem
.submitter
display: block
font-size: 0.8rem
color: $text-muted
margin-top: 0.5rem
font-style: italic
.versus .versus
font-family: 'Bangers', cursive font-family: 'Bangers', cursive

View File

@ -53,8 +53,17 @@
border: 3px solid #000 border: 3px solid #000
position: relative position: relative
cursor: pointer cursor: pointer
box-shadow: 3px 3px 0 rgba(0, 0, 0, 0.2)
transition: all 0.2s ease
&:checked:after &:hover
border-color: $secondary
&:checked
background-color: rgba($primary, 0.2)
border-color: $primary
&:after
content: '' content: ''
position: absolute position: absolute
left: 6px left: 6px
@ -86,6 +95,31 @@
gap: 1rem gap: 1rem
margin-top: 1.5rem margin-top: 1.5rem
// Checkbox group with helper text
.checkbox-group
display: flex
flex-direction: column
margin-bottom: 1.5rem
.checkbox-container
display: flex
align-items: center
margin-bottom: 0.5rem
input[type="checkbox"]
margin-right: 0.75rem
min-width: 24px
height: 24px
label
margin-bottom: 0
margin-top: 0
cursor: pointer
font-family: 'Press Start 2P', monospace
font-size: 0.8rem
text-transform: uppercase
color: $secondary
// Buttons // Buttons
.btn .btn
display: inline-flex display: inline-flex

View File

@ -89,7 +89,7 @@ const HomeScreen = () => {
</div> </div>
<footer className="home-footer"> <footer className="home-footer">
<p>ich weiß doch auch nicht</p> <p>ik wes doch och nüsch</p>
</footer> </footer>
</div> </div>
); );

View File

@ -2,13 +2,15 @@ import { useState, useEffect } 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 { faUsers, faGear, faPlay, faCopy, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; import { faUsers, faGear, faPlay, faCopy, faSignOutAlt } from '@fortawesome/free-solid-svg-icons';
import { getDisplayName } from '../utils/playerUtils';
const LobbyScreen = () => { const LobbyScreen = () => {
const { lobby, currentPlayer, isHost, updateSettings, startGame, leaveLobby } = useGame(); const { lobby, currentPlayer, isHost, updateSettings, startGame, leaveLobby } = useGame();
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
const [settings, setSettings] = useState({ const [settings, setSettings] = useState({
songsPerPlayer: 4, songsPerPlayer: 4,
maxPlayers: 10 maxPlayers: 10,
hidePlayerNames: false
}); });
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -64,9 +66,11 @@ const LobbyScreen = () => {
<div className="players-list"> <div className="players-list">
<h2><FontAwesomeIcon icon={faUsers} /> Spieler ({lobby.players.length})</h2> <h2><FontAwesomeIcon icon={faUsers} /> Spieler ({lobby.players.length})</h2>
<ul> <ul>
{lobby.players.map(player => ( {lobby.players.map((player, index) => (
<li key={player.id} className={`player ${!player.isConnected ? 'disconnected' : ''} ${player.id === lobby.hostId ? 'host' : ''}`}> <li key={player.id} className={`player ${!player.isConnected ? 'disconnected' : ''} ${player.id === lobby.hostId ? 'host' : ''}`}>
{player.name} {player.id === lobby.hostId && '(Gastgeber)'} {player.id === currentPlayer.id && '(Du)'} {getDisplayName(player, lobby, currentPlayer, index)}
{player.id === lobby.hostId && ' (Gastgeber)'}
{player.id === currentPlayer.id && !lobby.settings.hidePlayerNames && ' (Du)'}
{!player.isConnected && <span className="status-disconnected">(Getrennt)</span>} {!player.isConnected && <span className="status-disconnected">(Getrennt)</span>}
</li> </li>
))} ))}
@ -78,6 +82,7 @@ const LobbyScreen = () => {
<h3><FontAwesomeIcon icon={faGear} /> Spieleinstellungen</h3> <h3><FontAwesomeIcon icon={faGear} /> Spieleinstellungen</h3>
<p>Lieder pro Spieler: {settings.songsPerPlayer}</p> <p>Lieder pro Spieler: {settings.songsPerPlayer}</p>
<p>Maximale Spieler: {settings.maxPlayers}</p> <p>Maximale Spieler: {settings.maxPlayers}</p>
<p>Spielernamen verbergen: {settings.hidePlayerNames ? 'Ja' : 'Nein'}</p>
{isHost && ( {isHost && (
<button className="btn secondary" onClick={() => setShowSettings(true)}> <button className="btn secondary" onClick={() => setShowSettings(true)}>
@ -138,6 +143,19 @@ const LobbyScreen = () => {
/> />
</div> </div>
<div className="form-group checkbox-group">
<div className="checkbox-container">
<input
type="checkbox"
id="hidePlayerNames"
name="hidePlayerNames"
checked={settings.hidePlayerNames}
onChange={handleSettingsChange}
/>
<label htmlFor="hidePlayerNames">Spielernamen verbergen</label>
</div>
</div>
<div className="modal-actions"> <div className="modal-actions">
<button className="btn secondary" onClick={() => setShowSettings(false)}> <button className="btn secondary" onClick={() => setShowSettings(false)}>
Abbrechen Abbrechen

View File

@ -3,6 +3,7 @@ import { useGame } from '../context/GameContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTrophy, faHome, faRedo, faChartLine } from '@fortawesome/free-solid-svg-icons'; import { faTrophy, faHome, faRedo, faChartLine } from '@fortawesome/free-solid-svg-icons';
import YouTubeEmbed from './YouTubeEmbed'; import YouTubeEmbed from './YouTubeEmbed';
import { getDisplayName } from '../utils/playerUtils';
const ResultsScreen = () => { const ResultsScreen = () => {
const { lobby, currentPlayer, leaveLobby } = useGame(); const { lobby, currentPlayer, leaveLobby } = useGame();
@ -73,6 +74,12 @@ const ResultsScreen = () => {
return (match && match[2].length === 11) ? match[2] : null; return (match && match[2].length === 11) ? match[2] : null;
} }
// Find the submitter from the player list
const findSubmitter = (submitterId) => {
if (!lobby || !lobby.players) return null;
return lobby.players.find(player => player.id === submitterId);
};
if (!winner) { if (!winner) {
return ( return (
<div className="results-screen"> <div className="results-screen">
@ -81,6 +88,9 @@ const ResultsScreen = () => {
); );
} }
// Find the player who submitted the winning song
const submitter = findSubmitter(winner.submittedBy);
return ( return (
<div className="results-screen"> <div className="results-screen">
<header> <header>
@ -91,7 +101,11 @@ const ResultsScreen = () => {
<div className="winner-info"> <div className="winner-info">
<h2>{winner.title}</h2> <h2>{winner.title}</h2>
<h3>von {winner.artist}</h3> <h3>von {winner.artist}</h3>
<p className="submitter">Eingereicht von: {winner.submittedByName}</p> <p className="submitter">
Eingereicht von: {submitter
? getDisplayName(submitter, lobby, currentPlayer)
: (lobby?.settings?.hidePlayerNames ? 'Anonymer Spieler' : winner.submittedByName)}
</p>
</div> </div>
{winnerVideoId ? ( {winnerVideoId ? (
@ -129,6 +143,10 @@ const ResultsScreen = () => {
const song1VideoId = getYouTubeId(battle.song1?.youtubeLink); const song1VideoId = getYouTubeId(battle.song1?.youtubeLink);
const song2VideoId = battle.song2 ? getYouTubeId(battle.song2.youtubeLink) : null; const song2VideoId = battle.song2 ? getYouTubeId(battle.song2.youtubeLink) : null;
// Find the players who submitted the songs
const song1Submitter = findSubmitter(battle.song1?.submittedBy);
const song2Submitter = battle.song2 ? findSubmitter(battle.song2?.submittedBy) : null;
// Freilos-Runden behandeln // Freilos-Runden behandeln
const isByeRound = battle.bye === true || !battle.song2; const isByeRound = battle.bye === true || !battle.song2;
@ -144,6 +162,13 @@ const ResultsScreen = () => {
<h5>{battle.song1.title}</h5> <h5>{battle.song1.title}</h5>
<p>{battle.song1.artist}</p> <p>{battle.song1.artist}</p>
<span className="votes">{battle.song1Votes} Stimmen</span> <span className="votes">{battle.song1Votes} Stimmen</span>
{!lobby?.settings?.hidePlayerNames && (
<span className="submitter">
Eingereicht von: {song1Submitter
? getDisplayName(song1Submitter, lobby, currentPlayer)
: battle.song1?.submittedByName || 'Unbekannt'}
</span>
)}
</div> </div>
</div> </div>
@ -155,6 +180,13 @@ const ResultsScreen = () => {
<h5>{battle.song2.title}</h5> <h5>{battle.song2.title}</h5>
<p>{battle.song2.artist}</p> <p>{battle.song2.artist}</p>
<span className="votes">{battle.song2Votes} Stimmen</span> <span className="votes">{battle.song2Votes} Stimmen</span>
{!lobby?.settings?.hidePlayerNames && (
<span className="submitter">
Eingereicht von: {song2Submitter
? getDisplayName(song2Submitter, lobby, currentPlayer)
: battle.song2?.submittedByName || 'Unbekannt'}
</span>
)}
</div> </div>
</div> </div>
) : ( ) : (

View File

@ -2,6 +2,7 @@ import { useState, useEffect, useRef } 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 { faPlus, faTrash, faCheck, faMusic, faVideoCamera, faSearch, faSpinner, faExternalLinkAlt, faTimes } from '@fortawesome/free-solid-svg-icons'; import { faPlus, faTrash, faCheck, faMusic, faVideoCamera, faSearch, faSpinner, faExternalLinkAlt, faTimes } from '@fortawesome/free-solid-svg-icons';
import { getDisplayName } from '../utils/playerUtils';
const SongSubmissionScreen = () => { const SongSubmissionScreen = () => {
const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube, getYouTubeMetadata } = useGame(); const { lobby, currentPlayer, addSong, removeSong, setPlayerReady, searchYouTube, getYouTubeMetadata } = useGame();
@ -502,9 +503,9 @@ const SongSubmissionScreen = () => {
<div className="player-status"> <div className="player-status">
<h4>Spieler bereit</h4> <h4>Spieler bereit</h4>
<ul className="players-ready-list"> <ul className="players-ready-list">
{lobby && lobby.players.map(player => ( {lobby && lobby.players.map((player, index) => (
<li key={player.id} className={player.isReady ? 'ready' : 'not-ready'}> <li key={player.id} className={player.isReady ? 'ready' : 'not-ready'}>
{player.name} {player.id === currentPlayer.id && '(Du)'} {getDisplayName(player, lobby, currentPlayer, index)} {player.id === currentPlayer.id && lobby.settings.hidePlayerNames && '(Du)'}
{player.isReady ? ( {player.isReady ? (
<FontAwesomeIcon icon={faCheck} className="ready-icon" /> <FontAwesomeIcon icon={faCheck} className="ready-icon" />
) : ( ) : (

View File

@ -4,6 +4,7 @@ import { useGame } from '../context/GameContext';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; import { faVoteYea, faTrophy, faMusic, faCheck, faMedal, faCrown, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import YouTubeEmbed from './YouTubeEmbed'; import YouTubeEmbed from './YouTubeEmbed';
import { getDisplayName } from '../utils/playerUtils';
function VotingScreen() { function VotingScreen() {
const { lobby, currentPlayer, submitVote, isHost, isOffline, isReconnecting } = useGame(); const { lobby, currentPlayer, submitVote, isHost, isOffline, isReconnecting } = useGame();
@ -356,7 +357,7 @@ function VotingScreen() {
return ( return (
<li key={player.id} className={`${hasPlayerVoted ? 'voted' : 'not-voted'} ${isCurrentPlayerOfflineVoted ? 'offline-voted' : ''}`}> <li key={player.id} className={`${hasPlayerVoted ? 'voted' : 'not-voted'} ${isCurrentPlayerOfflineVoted ? 'offline-voted' : ''}`}>
{player.name} {player.id === currentPlayer.id && '(Du)'} {getDisplayName(player, lobby, currentPlayer)} {player.id === currentPlayer.id && lobby.settings.hidePlayerNames && '(Du)'}
{hasPlayerVoted && {hasPlayerVoted &&
<FontAwesomeIcon icon={faCheck} className="vote-icon" /> <FontAwesomeIcon icon={faCheck} className="vote-icon" />
} }

View File

@ -0,0 +1,45 @@
/**
* Utility functions for player name handling
*/
/**
* Returns an anonymous name for a player when hidePlayerNames is enabled
* @param {string} playerId - The player's ID
* @param {number} index - The player's index in a list (optional)
* @param {boolean} isCurrentPlayer - Whether this is the current player
* @returns {string} - Anonymous name or indicator
*/
export const getAnonymousName = (playerId, index = null, isCurrentPlayer = false) => {
if (isCurrentPlayer) {
return 'Du';
}
// Use index-based naming if provided
if (index !== null) {
return `Spieler ${index + 1}`;
}
// Use last 4 characters of ID as fallback
const shortId = playerId.slice(-4);
return `Spieler ${shortId}`;
};
/**
* Returns the appropriate player name based on game settings
* @param {Object} player - The player object
* @param {Object} lobby - The lobby object
* @param {Object} currentPlayer - The current player
* @param {number} index - The player's index in a list (optional)
* @returns {string} - The player name or anonymous identifier
*/
export const getDisplayName = (player, lobby, currentPlayer, index = null) => {
const isCurrentPlayer = player.id === currentPlayer?.id;
// If hidePlayerNames is enabled
if (lobby?.settings?.hidePlayerNames) {
return getAnonymousName(player.id, index, isCurrentPlayer);
}
// Otherwise return the actual name
return player.name;
};

View File

@ -28,7 +28,8 @@ class GameManager {
settings: { settings: {
songsPerPlayer: 3, songsPerPlayer: 3,
maxPlayers: 10, maxPlayers: 10,
minPlayers: 3 minPlayers: 3,
hidePlayerNames: false
}, },
players: [{ players: [{
id: hostId, id: hostId,
@ -225,6 +226,11 @@ class GameManager {
return { error: 'Max players must be between 3 and 20' }; return { error: 'Max players must be between 3 and 20' };
} }
// Validate boolean settings
if (settings.hidePlayerNames !== undefined && typeof settings.hidePlayerNames !== 'boolean') {
settings.hidePlayerNames = Boolean(settings.hidePlayerNames);
}
// Update settings // Update settings
lobby.settings = { lobby.settings = {
...lobby.settings, ...lobby.settings,