Compare commits
No commits in common. "032ebc2368bc6eb3679c4c75cbc027e0ef8dd547" and "2fb5af41710a204406c54eeb3106bfac12eaffcc" have entirely different histories.
032ebc2368
...
2fb5af4171
@ -5,7 +5,6 @@ import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
|||||||
import {faDrum, faGuitar, faHeadphones, faMusic} from "@fortawesome/free-solid-svg-icons";
|
import {faDrum, faGuitar, faHeadphones, faMusic} from "@fortawesome/free-solid-svg-icons";
|
||||||
import Home from "@/pages/Home";
|
import Home from "@/pages/Home";
|
||||||
import WaitingRoom from "@/pages/WaitingRoom";
|
import WaitingRoom from "@/pages/WaitingRoom";
|
||||||
import Ending from "@/pages/Ending";
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const {currentState} = useContext(StateContext);
|
const {currentState} = useContext(StateContext);
|
||||||
@ -64,7 +63,6 @@ const App = () => {
|
|||||||
{currentState === "WaitingRoom" && <WaitingRoom />}
|
{currentState === "WaitingRoom" && <WaitingRoom />}
|
||||||
{currentState === "Home" && <Home />}
|
{currentState === "Home" && <Home />}
|
||||||
{currentState === "Game" && <Game />}
|
{currentState === "Game" && <Game />}
|
||||||
{currentState === "Ending" && <Ending />}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,72 +0,0 @@
|
|||||||
import { useEffect, useState, useContext } from "react";
|
|
||||||
import { StateContext } from "@/common/contexts/StateContext";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { faTrophy, faMedal, faAward, faHome, faCrown } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import "./styles.sass";
|
|
||||||
|
|
||||||
export const Ending = () => {
|
|
||||||
const { setCurrentState } = useContext(StateContext);
|
|
||||||
const [finalScores, setFinalScores] = useState([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const savedData = JSON.parse(localStorage.getItem('finalScores') || '{"scores":{}}');
|
|
||||||
const sortedScores = Object.entries(savedData.scores)
|
|
||||||
.map(([userId, data]) => ({
|
|
||||||
id: userId,
|
|
||||||
name: data.name,
|
|
||||||
score: data.score
|
|
||||||
}))
|
|
||||||
.sort((a, b) => b.score - a.score);
|
|
||||||
setFinalScores(sortedScores);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getPlayerIcon = (index) => {
|
|
||||||
switch(index) {
|
|
||||||
case 0: return <FontAwesomeIcon icon={faTrophy} className="gold" />;
|
|
||||||
case 1: return <FontAwesomeIcon icon={faMedal} className="silver" />;
|
|
||||||
case 2: return <FontAwesomeIcon icon={faAward} className="bronze" />;
|
|
||||||
default: return <FontAwesomeIcon icon={faCrown} className="normal" />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleReturnHome = () => {
|
|
||||||
localStorage.removeItem('finalScores');
|
|
||||||
setCurrentState("Home");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="ending-page">
|
|
||||||
<div className="background-overlay">
|
|
||||||
<div className="rotating-gradient"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="ending-content">
|
|
||||||
<h1>Spiel beendet!</h1>
|
|
||||||
<div className="final-scores">
|
|
||||||
<h2>Endstand</h2>
|
|
||||||
<div className="leaderboard">
|
|
||||||
{finalScores.map((player, index) => (
|
|
||||||
<div
|
|
||||||
key={player.id}
|
|
||||||
className={`leaderboard-entry ${index < 3 ? `top-${index + 1}` : ''}`}
|
|
||||||
>
|
|
||||||
<div className="rank-icon">
|
|
||||||
{getPlayerIcon(index)}
|
|
||||||
</div>
|
|
||||||
<div className="player-info">
|
|
||||||
<span className="player-name">{player.name}</span>
|
|
||||||
<span className="player-score">{player.score} Punkte</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="return-home" onClick={handleReturnHome}>
|
|
||||||
<FontAwesomeIcon icon={faHome} />
|
|
||||||
Zurück zum Hauptmenü
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export {Ending as default} from "./Ending.jsx";
|
|
@ -1,128 +0,0 @@
|
|||||||
@import "@/common/styles/colors"
|
|
||||||
|
|
||||||
.ending-page
|
|
||||||
height: 100vh
|
|
||||||
width: 100vw
|
|
||||||
display: flex
|
|
||||||
justify-content: center
|
|
||||||
align-items: center
|
|
||||||
position: relative
|
|
||||||
|
|
||||||
.ending-content
|
|
||||||
text-align: center
|
|
||||||
z-index: 2
|
|
||||||
animation: float-up 0.8s ease-out
|
|
||||||
|
|
||||||
h1
|
|
||||||
font-size: 48pt
|
|
||||||
color: $white
|
|
||||||
margin-bottom: 40px
|
|
||||||
background: linear-gradient(135deg, $yellow, $pink)
|
|
||||||
-webkit-background-clip: text
|
|
||||||
background-clip: text
|
|
||||||
-webkit-text-fill-color: transparent
|
|
||||||
animation: title-shimmer 3s infinite alternate ease-in-out
|
|
||||||
|
|
||||||
.final-scores
|
|
||||||
background: rgba(255, 255, 255, 0.1)
|
|
||||||
backdrop-filter: blur(10px)
|
|
||||||
border-radius: 20px
|
|
||||||
padding: 30px
|
|
||||||
margin-bottom: 30px
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
|
||||||
|
|
||||||
h2
|
|
||||||
color: $white
|
|
||||||
margin-bottom: 20px
|
|
||||||
font-size: 24pt
|
|
||||||
|
|
||||||
.leaderboard
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
gap: 15px
|
|
||||||
|
|
||||||
.leaderboard-entry
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
padding: 15px 20px
|
|
||||||
background: rgba(255, 255, 255, 0.05)
|
|
||||||
border-radius: 15px
|
|
||||||
transition: all 0.3s ease
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
transform: translateY(-2px)
|
|
||||||
background: rgba(255, 255, 255, 0.1)
|
|
||||||
|
|
||||||
&.top-1
|
|
||||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2), rgba(255, 215, 0, 0.1))
|
|
||||||
border-color: rgba(255, 215, 0, 0.3)
|
|
||||||
transform: scale(1.05)
|
|
||||||
|
|
||||||
.rank-icon
|
|
||||||
color: #FFD700
|
|
||||||
filter: drop-shadow(0 0 10px rgba(255, 215, 0, 0.5))
|
|
||||||
|
|
||||||
&.top-2
|
|
||||||
background: linear-gradient(135deg, rgba(192, 192, 192, 0.2), rgba(192, 192, 192, 0.1))
|
|
||||||
border-color: rgba(192, 192, 192, 0.3)
|
|
||||||
|
|
||||||
.rank-icon
|
|
||||||
color: #C0C0C0
|
|
||||||
filter: drop-shadow(0 0 10px rgba(192, 192, 192, 0.5))
|
|
||||||
|
|
||||||
&.top-3
|
|
||||||
background: linear-gradient(135deg, rgba(205, 127, 50, 0.2), rgba(205, 127, 50, 0.1))
|
|
||||||
border-color: rgba(205, 127, 50, 0.3)
|
|
||||||
|
|
||||||
.rank-icon
|
|
||||||
color: #CD7F32
|
|
||||||
filter: drop-shadow(0 0 10px rgba(205, 127, 50, 0.5))
|
|
||||||
|
|
||||||
.rank-icon
|
|
||||||
font-size: 24pt
|
|
||||||
margin-right: 20px
|
|
||||||
|
|
||||||
.player-info
|
|
||||||
flex: 1
|
|
||||||
display: flex
|
|
||||||
justify-content: space-between
|
|
||||||
align-items: center
|
|
||||||
|
|
||||||
.player-name
|
|
||||||
color: $white
|
|
||||||
font-size: 18pt
|
|
||||||
|
|
||||||
.player-score
|
|
||||||
color: $yellow
|
|
||||||
font-size: 16pt
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.return-home
|
|
||||||
padding: 15px 30px
|
|
||||||
background: linear-gradient(135deg, $purple, $blue)
|
|
||||||
border: none
|
|
||||||
border-radius: 12px
|
|
||||||
color: $white
|
|
||||||
font-size: 16pt
|
|
||||||
cursor: pointer
|
|
||||||
transition: all 0.3s ease
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
gap: 10px
|
|
||||||
margin: 0 auto
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
transform: translateY(-3px)
|
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4)
|
|
||||||
|
|
||||||
svg
|
|
||||||
font-size: 18pt
|
|
||||||
|
|
||||||
@keyframes float-up
|
|
||||||
0%
|
|
||||||
opacity: 0
|
|
||||||
transform: translateY(30px)
|
|
||||||
100%
|
|
||||||
opacity: 1
|
|
||||||
transform: translateY(0)
|
|
@ -1,6 +1,5 @@
|
|||||||
import "./styles.sass";
|
import "./styles.sass";
|
||||||
import {SocketContext} from "@/common/contexts/SocketContext";
|
import {SocketContext} from "@/common/contexts/SocketContext";
|
||||||
import {StateContext} from "@/common/contexts/StateContext";
|
|
||||||
import {useContext, useState, useEffect, useRef, useCallback} from "react";
|
import {useContext, useState, useEffect, useRef, useCallback} from "react";
|
||||||
import MusicSlider from "@/pages/Game/components/MusicSlider";
|
import MusicSlider from "@/pages/Game/components/MusicSlider";
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
@ -11,14 +10,15 @@ import {
|
|||||||
faClock,
|
faClock,
|
||||||
faCrown,
|
faCrown,
|
||||||
faPaperPlane,
|
faPaperPlane,
|
||||||
faCheckCircle
|
faCheckCircle,
|
||||||
|
faVolumeUp,
|
||||||
|
faStepForward
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
|
import { fetchPlaylistSongs } from "@/services/youtubeService.js";
|
||||||
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
|
import YouTubePlayer from "../../components/YouTubePlayer/YouTubePlayer";
|
||||||
|
|
||||||
export const Game = () => {
|
export const Game = () => {
|
||||||
const {send, on, socket, connected, connect} = useContext(SocketContext);
|
const {send, on, socket, connected, connect} = useContext(SocketContext);
|
||||||
const {setCurrentState} = useContext(StateContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!connected) {
|
if (!connected) {
|
||||||
@ -69,14 +69,9 @@ export const Game = () => {
|
|||||||
},
|
},
|
||||||
"song-selected": setCurrentSong,
|
"song-selected": setCurrentSong,
|
||||||
"round-started": (data) => {
|
"round-started": (data) => {
|
||||||
requestAnimationFrame(() => {
|
setRound(data.round);
|
||||||
setPhase("composing");
|
setPhase("composing");
|
||||||
setRound(data.round);
|
setTimeLeft(data.timeRemaining);
|
||||||
setTimeLeft(data.timeRemaining);
|
|
||||||
setSelectedSong(null);
|
|
||||||
setHasGuessed(false);
|
|
||||||
setGuessResult(null);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
"guessing-phase-started": (data) => {
|
"guessing-phase-started": (data) => {
|
||||||
console.log("Guessing phase started:", data);
|
console.log("Guessing phase started:", data);
|
||||||
@ -135,7 +130,7 @@ export const Game = () => {
|
|||||||
if (phase === "composing") {
|
if (phase === "composing") {
|
||||||
console.log("Received frequency update:", data.frequency);
|
console.log("Received frequency update:", data.frequency);
|
||||||
setFrequency(data.frequency);
|
setFrequency(data.frequency);
|
||||||
setComposerIsPlaying(data.isPlaying);
|
setComposerIsPlaying(data.isPlaying); // Make sure isPlaying is handled
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"phase-changed": (data) => {
|
"phase-changed": (data) => {
|
||||||
@ -158,10 +153,6 @@ export const Game = () => {
|
|||||||
console.log("Received song options early:", data);
|
console.log("Received song options early:", data);
|
||||||
setSongOptions(data.songOptions || []);
|
setSongOptions(data.songOptions || []);
|
||||||
},
|
},
|
||||||
"game-ended": (finalData) => {
|
|
||||||
setCurrentState("Ending");
|
|
||||||
localStorage.setItem('finalScores', JSON.stringify(finalData));
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cleanupFunctions = Object.entries(eventHandlers).map(
|
const cleanupFunctions = Object.entries(eventHandlers).map(
|
||||||
@ -174,7 +165,7 @@ export const Game = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () => cleanupFunctions.forEach(cleanup => cleanup());
|
return () => cleanupFunctions.forEach(cleanup => cleanup());
|
||||||
}, [socket, on, send, role, currentSong, phase, connected, connectedUsers, setCurrentState, round]);
|
}, [socket, on, send, role, currentSong, phase, connected, connectedUsers]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
if (timerIntervalRef.current) clearInterval(timerIntervalRef.current);
|
||||||
@ -238,14 +229,6 @@ export const Game = () => {
|
|||||||
}
|
}
|
||||||
}, [allSongs, songOptions]);
|
}, [allSongs, songOptions]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (phase === 'composing') {
|
|
||||||
setSelectedSong(null);
|
|
||||||
setHasGuessed(false);
|
|
||||||
setGuessResult(null);
|
|
||||||
}
|
|
||||||
}, [phase]);
|
|
||||||
|
|
||||||
const handleFrequencyChange = useCallback((newFrequency, isPlaying) => {
|
const handleFrequencyChange = useCallback((newFrequency, isPlaying) => {
|
||||||
setFrequency(newFrequency);
|
setFrequency(newFrequency);
|
||||||
setComposerIsPlaying(isPlaying);
|
setComposerIsPlaying(isPlaying);
|
||||||
@ -285,8 +268,10 @@ export const Game = () => {
|
|||||||
}, [selectedSong, send, phase]);
|
}, [selectedSong, send, phase]);
|
||||||
|
|
||||||
const handleNextRound = useCallback(() => {
|
const handleNextRound = useCallback(() => {
|
||||||
setTimeLeft(0);
|
|
||||||
send("next-round");
|
send("next-round");
|
||||||
|
setSelectedSong(null);
|
||||||
|
setGuessResult(null);
|
||||||
|
setTimeLeft(0);
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
const handlePlayerReady = useCallback(() => {
|
const handlePlayerReady = useCallback(() => {
|
||||||
|
@ -1,21 +1,6 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import "./styles.sass";
|
import "./styles.sass";
|
||||||
|
|
||||||
const frequencyToNote = (frequency) => {
|
|
||||||
const A4 = 440;
|
|
||||||
const notes = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
|
||||||
|
|
||||||
const halfSteps = Math.round(12 * Math.log2(frequency / A4));
|
|
||||||
|
|
||||||
const A4Index = notes.indexOf('A');
|
|
||||||
let noteIndex = (A4Index + halfSteps) % 12;
|
|
||||||
if (noteIndex < 0) noteIndex += 12;
|
|
||||||
|
|
||||||
const octave = 4 + Math.floor((halfSteps + A4Index) / 12);
|
|
||||||
|
|
||||||
return `${notes[noteIndex]}${octave}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MusicSlider = ({ isReadOnly, onFrequencyChange, frequency: externalFrequency, composerIsPlaying }) => {
|
export const MusicSlider = ({ isReadOnly, onFrequencyChange, frequency: externalFrequency, composerIsPlaying }) => {
|
||||||
const [audioContext, setAudioContext] = useState(null);
|
const [audioContext, setAudioContext] = useState(null);
|
||||||
const [oscillator, setOscillator] = useState(null);
|
const [oscillator, setOscillator] = useState(null);
|
||||||
@ -202,25 +187,6 @@ export const MusicSlider = ({ isReadOnly, onFrequencyChange, frequency: external
|
|||||||
return `${Math.max(0, Math.min(pos, 100))}%`;
|
return `${Math.max(0, Math.min(pos, 100))}%`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMouthSize = () => {
|
|
||||||
const minSize = 30;
|
|
||||||
const maxSize = 60;
|
|
||||||
const freqRange = 880;
|
|
||||||
const sizeRange = maxSize - minSize;
|
|
||||||
const relativeFreq = frequency.current - 220;
|
|
||||||
const size = minSize + (relativeFreq / freqRange) * sizeRange;
|
|
||||||
return Math.max(minSize, Math.min(maxSize, size));
|
|
||||||
};
|
|
||||||
|
|
||||||
const shouldShowNoteMarker = () => {
|
|
||||||
if (!isReadOnly) return isDragging.current;
|
|
||||||
return composerIsPlaying;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isReadOnly && !composerIsPlaying) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="otamatone-container">
|
<div className="otamatone-container">
|
||||||
<div className="otamatone">
|
<div className="otamatone">
|
||||||
@ -229,32 +195,22 @@ export const MusicSlider = ({ isReadOnly, onFrequencyChange, frequency: external
|
|||||||
<div className="eye left-eye"></div>
|
<div className="eye left-eye"></div>
|
||||||
<div className="eye right-eye"></div>
|
<div className="eye right-eye"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="otamatone-mouth"></div>
|
||||||
className="otamatone-mouth"
|
|
||||||
style={{
|
|
||||||
width: `${getMouthSize()}px`,
|
|
||||||
height: `${getMouthSize()}px`
|
|
||||||
}}
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
{(!isReadOnly || composerIsPlaying) && (
|
<div
|
||||||
|
ref={sliderRef}
|
||||||
|
className={`otamatone-neck ${!isReadOnly ? 'interactive' : ''}`}
|
||||||
|
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={sliderRef}
|
className={`frequency-indicator ${isReadOnly ? 'read-only' : ''} ${composerIsPlaying ? 'active' : ''}`}
|
||||||
className={`otamatone-neck ${!isReadOnly ? 'interactive' : ''}`}
|
style={{ left: getFrequencyPosition() }}
|
||||||
style={{ cursor: isReadOnly ? 'default' : 'pointer' }}
|
|
||||||
>
|
>
|
||||||
<div
|
<div className="note-marker">
|
||||||
className={`frequency-indicator ${isReadOnly ? 'read-only' : ''} ${composerIsPlaying ? 'active' : ''}`}
|
{(!isReadOnly && isDragging.current) && Math.round(frequency.current)}
|
||||||
style={{ left: getFrequencyPosition() }}
|
|
||||||
>
|
|
||||||
{shouldShowNoteMarker() && (
|
|
||||||
<div className="note-marker">
|
|
||||||
{frequencyToNote(frequency.current)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -104,23 +104,10 @@
|
|||||||
.note-marker
|
.note-marker
|
||||||
position: absolute
|
position: absolute
|
||||||
top: -50px
|
top: -50px
|
||||||
font-size: 24pt
|
font-size: 32pt
|
||||||
color: $white
|
color: $white
|
||||||
text-shadow: 0 0 15px rgba(255, 255, 255, 0.7)
|
text-shadow: 0 0 15px rgba(255, 255, 255, 0.7)
|
||||||
font-weight: bold
|
font-weight: bold
|
||||||
font-family: 'Arial', sans-serif
|
|
||||||
background: rgba(0, 0, 0, 0.6)
|
|
||||||
padding: 2px 8px
|
|
||||||
border-radius: 8px
|
|
||||||
backdrop-filter: blur(5px)
|
|
||||||
min-width: 60px
|
|
||||||
text-align: center
|
|
||||||
animation: note-pop 0.2s ease-out
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
.read-only &
|
|
||||||
background: rgba(0, 0, 0, 0.8)
|
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.4)
|
|
||||||
|
|
||||||
.otamatone-face
|
.otamatone-face
|
||||||
width: 140px
|
width: 140px
|
||||||
@ -182,11 +169,10 @@
|
|||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
position: absolute
|
position: absolute
|
||||||
bottom: 30px
|
bottom: 30px
|
||||||
transition: all 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
width: 30px
|
||||||
transform-origin: center center
|
height: 30px
|
||||||
|
transition: all 0.3s ease
|
||||||
&.active
|
animation: mouth-pulse 5s infinite alternate ease-in-out
|
||||||
animation: mouth-pulse 0.5s infinite alternate ease-in-out
|
|
||||||
|
|
||||||
@keyframes blink
|
@keyframes blink
|
||||||
0%, 90%, 100%
|
0%, 90%, 100%
|
||||||
@ -215,13 +201,3 @@
|
|||||||
100%
|
100%
|
||||||
transform: translateY(0)
|
transform: translateY(0)
|
||||||
opacity: 1
|
opacity: 1
|
||||||
|
|
||||||
@keyframes note-pop
|
|
||||||
0%
|
|
||||||
transform: scale(0.8)
|
|
||||||
opacity: 0.5
|
|
||||||
50%
|
|
||||||
transform: scale(1.1)
|
|
||||||
100%
|
|
||||||
transform: scale(1)
|
|
||||||
opacity: 1
|
|
@ -15,17 +15,14 @@ export const WaitingRoom = () => {
|
|||||||
const [isHost, setIsHost] = useState(false);
|
const [isHost, setIsHost] = useState(false);
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [playlists, setPlaylists] = useState({});
|
|
||||||
const [votes, setVotes] = useState({});
|
|
||||||
const [selectedPlaylist, setSelectedPlaylist] = useState(null);
|
|
||||||
const messageEndRef = useRef(null);
|
const messageEndRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Check if the user is a host and get other initial data
|
||||||
send("check-host-status");
|
send("check-host-status");
|
||||||
send("get-user-info");
|
send("get-user-info");
|
||||||
send("get-room-users");
|
send("get-room-users");
|
||||||
send("get-room-code");
|
send("get-room-code");
|
||||||
send("get-playlist-options");
|
|
||||||
|
|
||||||
const handleHostStatus = (status) => {
|
const handleHostStatus = (status) => {
|
||||||
setIsHost(status.isHost);
|
setIsHost(status.isHost);
|
||||||
@ -75,39 +72,7 @@ export const WaitingRoom = () => {
|
|||||||
setCurrentState("Game");
|
setCurrentState("Game");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePlaylistOptions = (options) => {
|
// Register event listeners
|
||||||
const playlistsWithVotes = { ...options };
|
|
||||||
Object.keys(playlistsWithVotes).forEach(genre => {
|
|
||||||
const playlistId = playlistsWithVotes[genre].id;
|
|
||||||
playlistsWithVotes[genre].votes = votes[playlistId]?.length || 0;
|
|
||||||
});
|
|
||||||
setPlaylists(playlistsWithVotes);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleVotesUpdated = (newVotes) => {
|
|
||||||
console.log('Received updated votes:', newVotes);
|
|
||||||
setVotes(newVotes);
|
|
||||||
|
|
||||||
setPlaylists(current => {
|
|
||||||
const updated = { ...current };
|
|
||||||
Object.keys(updated).forEach(genre => {
|
|
||||||
const playlistId = updated[genre].id;
|
|
||||||
updated[genre].votes = newVotes[playlistId]?.length || 0;
|
|
||||||
});
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
|
|
||||||
Object.entries(newVotes).forEach(([playlistId, voters]) => {
|
|
||||||
if (voters.includes(socket.id)) {
|
|
||||||
setSelectedPlaylist(playlistId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRoomJoined = (code) => {
|
|
||||||
setRoomCode(code);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cleanupHostStatus = on("host-status", handleHostStatus);
|
const cleanupHostStatus = on("host-status", handleHostStatus);
|
||||||
const cleanupUserInfo = on("user-info", handleUserInfo);
|
const cleanupUserInfo = on("user-info", handleUserInfo);
|
||||||
const cleanupRoomUsers = on("room-users", handleRoomUsers);
|
const cleanupRoomUsers = on("room-users", handleRoomUsers);
|
||||||
@ -117,10 +82,8 @@ export const WaitingRoom = () => {
|
|||||||
const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected);
|
const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected);
|
||||||
const cleanupChatMessage = on("chat-message", handleChatMessage);
|
const cleanupChatMessage = on("chat-message", handleChatMessage);
|
||||||
const cleanupGameStarted = on("game-started", handleGameStarted);
|
const cleanupGameStarted = on("game-started", handleGameStarted);
|
||||||
const cleanupPlaylistOptions = on("playlist-options", handlePlaylistOptions);
|
|
||||||
const cleanupVotesUpdated = on("playlist-votes-updated", handleVotesUpdated);
|
|
||||||
const cleanupRoomJoined = on("room-joined", handleRoomJoined);
|
|
||||||
|
|
||||||
|
// Add welcome message
|
||||||
setMessages([{
|
setMessages([{
|
||||||
system: true,
|
system: true,
|
||||||
text: "Welcome to the waiting room! Waiting for others to join..."
|
text: "Welcome to the waiting room! Waiting for others to join..."
|
||||||
@ -136,9 +99,6 @@ export const WaitingRoom = () => {
|
|||||||
cleanupUserDisconnected();
|
cleanupUserDisconnected();
|
||||||
cleanupChatMessage();
|
cleanupChatMessage();
|
||||||
cleanupGameStarted();
|
cleanupGameStarted();
|
||||||
cleanupPlaylistOptions();
|
|
||||||
cleanupVotesUpdated();
|
|
||||||
cleanupRoomJoined();
|
|
||||||
};
|
};
|
||||||
}, [on, send, socket, setCurrentState]);
|
}, [on, send, socket, setCurrentState]);
|
||||||
|
|
||||||
@ -165,6 +125,7 @@ export const WaitingRoom = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLeaveRoom = () => {
|
const handleLeaveRoom = () => {
|
||||||
|
// Disconnect from the current room
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
}
|
}
|
||||||
@ -177,61 +138,7 @@ export const WaitingRoom = () => {
|
|||||||
setTimeout(() => setCopied(false), 2000);
|
setTimeout(() => setCopied(false), 2000);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleVote = (playlistId) => {
|
const minPlayersToStart = 1; // Set your minimum players requirement here
|
||||||
setSelectedPlaylist(playlistId);
|
|
||||||
send("vote-playlist", { playlistId });
|
|
||||||
};
|
|
||||||
|
|
||||||
const getVoteCount = (playlistId) => {
|
|
||||||
return votes[playlistId]?.length || 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPlaylistSection = () => (
|
|
||||||
<div className="playlist-section">
|
|
||||||
<div className="section-header">
|
|
||||||
<h2>Choose a Playlist</h2>
|
|
||||||
<p className="vote-info">Most voted playlist will be used for the game</p>
|
|
||||||
</div>
|
|
||||||
<div className="playlists-grid">
|
|
||||||
{Object.entries(playlists).map(([genre, playlist]) => (
|
|
||||||
<div
|
|
||||||
key={playlist.id}
|
|
||||||
className={`playlist-card ${selectedPlaylist === playlist.id ? 'selected' : ''}`}
|
|
||||||
onClick={() => handleVote(playlist.id)}
|
|
||||||
>
|
|
||||||
<div className="playlist-thumbnail">
|
|
||||||
<img src={playlist.thumbnail.url} alt={playlist.title} />
|
|
||||||
<div className="playlist-overlay">
|
|
||||||
<div className="vote-count">
|
|
||||||
<span className="count">{getVoteCount(playlist.id)}</span>
|
|
||||||
<span className="vote-label">votes</span>
|
|
||||||
</div>
|
|
||||||
{selectedPlaylist === playlist.id && (
|
|
||||||
<div className="your-vote">Your Vote</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="playlist-info">
|
|
||||||
<h3>{genre.toUpperCase()}</h3>
|
|
||||||
<div className="playlist-details">
|
|
||||||
<span>{playlist.songCount} songs</span>
|
|
||||||
<div className="vote-percentage-bar">
|
|
||||||
<div
|
|
||||||
className="fill"
|
|
||||||
style={{
|
|
||||||
width: `${(getVoteCount(playlist.id) / Math.max(1, users.length)) * 100}%`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const minPlayersToStart = 1;
|
|
||||||
const canStartGame = isHost && users.length >= minPlayersToStart;
|
const canStartGame = isHost && users.length >= minPlayersToStart;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -246,84 +153,78 @@ export const WaitingRoom = () => {
|
|||||||
<span>Zurück zur Startseite</span>
|
<span>Zurück zur Startseite</span>
|
||||||
</button>
|
</button>
|
||||||
<h1>Warteraum</h1>
|
<h1>Warteraum</h1>
|
||||||
{roomCode && (
|
<div className="room-code-container">
|
||||||
<div className="room-code-container">
|
<div className="room-code" onClick={copyRoomCode}>
|
||||||
<div className="room-code" onClick={copyRoomCode}>
|
Code: <span className="code">{roomCode}</span>
|
||||||
Code: <span className="code">{roomCode}</span>
|
<FontAwesomeIcon icon={copied ? faCheck : faCopy} className={copied ? "copied" : ""} />
|
||||||
<FontAwesomeIcon icon={copied ? faCheck : faCopy} className={copied ? "copied" : ""} />
|
|
||||||
</div>
|
|
||||||
<div className="copy-hint">{copied ? "Kopiert!" : "Klicken zum Kopieren"}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="copy-hint">{copied ? "Kopiert!" : "Klicken zum Kopieren"}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="waiting-room-layout">
|
<div className="waiting-room-content">
|
||||||
{renderPlaylistSection()}
|
<div className="users-panel">
|
||||||
|
<div className="panel-header">
|
||||||
<div className="bottom-section">
|
<FontAwesomeIcon icon={faUsers} />
|
||||||
<div className="users-panel">
|
<h2>Spieler ({users.length})</h2>
|
||||||
<div className="panel-header">
|
</div>
|
||||||
<FontAwesomeIcon icon={faUsers} />
|
<div className="users-list">
|
||||||
<h2>Spieler ({users.length})</h2>
|
{users.length === 0 ? (
|
||||||
</div>
|
<div className="no-users">Keine Spieler im Raum</div>
|
||||||
<div className="users-list">
|
) : (
|
||||||
{users.length === 0 ? (
|
users.map((user) => (
|
||||||
<div className="no-users">Keine Spieler im Raum</div>
|
<div key={user.id} className={`user-item ${user.creator ? 'host' : ''}`}>
|
||||||
) : (
|
{user.name} {user.creator && <span className="host-badge">Host</span>}
|
||||||
users.map((user) => (
|
</div>
|
||||||
<div key={user.id} className={`user-item ${user.creator ? 'host' : ''}`}>
|
))
|
||||||
{user.name} {user.creator && <span className="host-badge">Host</span>}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{isHost && (
|
|
||||||
<div className="game-controls">
|
|
||||||
<button
|
|
||||||
className={`start-game-button ${canStartGame ? '' : 'disabled'}`}
|
|
||||||
onClick={handleStartGame}
|
|
||||||
disabled={!canStartGame}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faPlayCircle} />
|
|
||||||
Spiel starten
|
|
||||||
</button>
|
|
||||||
{!canStartGame && users.length < minPlayersToStart && (
|
|
||||||
<div className="start-hint">Mindestens {minPlayersToStart} Spieler benötigt</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{isHost && (
|
||||||
|
<div className="game-controls">
|
||||||
|
<button
|
||||||
|
className={`start-game-button ${canStartGame ? '' : 'disabled'}`}
|
||||||
|
onClick={handleStartGame}
|
||||||
|
disabled={!canStartGame}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faPlayCircle} />
|
||||||
|
Spiel starten
|
||||||
|
</button>
|
||||||
|
{!canStartGame && users.length < minPlayersToStart && (
|
||||||
|
<div className="start-hint">Mindestens {minPlayersToStart} Spieler benötigt</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="chat-panel">
|
<div className="chat-panel">
|
||||||
<div className="panel-header">
|
<div className="panel-header">
|
||||||
<FontAwesomeIcon icon={faMessage} />
|
<FontAwesomeIcon icon={faMessage} />
|
||||||
<h2>Chat</h2>
|
<h2>Chat</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-messages">
|
<div className="chat-messages">
|
||||||
{messages.map((message, index) => (
|
{messages.map((message, index) => (
|
||||||
<div key={index} className={`message ${message.system ? 'system-message' : ''}`}>
|
<div key={index} className={`message ${message.system ? 'system-message' : ''}`}>
|
||||||
{message.system ? (
|
{message.system ? (
|
||||||
<span className="message-text system">{message.text}</span>
|
<span className="message-text system">{message.text}</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="message-sender">{message.sender}:</span>
|
<span className="message-sender">{message.sender}:</span>
|
||||||
<span className="message-text">{message.text}</span>
|
<span className="message-text">{message.text}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={messageEndRef}></div>
|
<div ref={messageEndRef}></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="chat-input">
|
<div className="chat-input">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
|
onKeyPress={(e) => e.key === 'Enter' && handleSendMessage()}
|
||||||
placeholder="Gib eine Nachricht ein..."
|
placeholder="Gib eine Nachricht ein..."
|
||||||
/>
|
/>
|
||||||
<button onClick={handleSendMessage}>Senden</button>
|
<button onClick={handleSendMessage}>Senden</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -102,11 +102,10 @@
|
|||||||
width: 100%
|
width: 100%
|
||||||
max-width: 1200px
|
max-width: 1200px
|
||||||
gap: 30px
|
gap: 30px
|
||||||
height: calc(100vh - 140px)
|
height: calc(100vh - 180px)
|
||||||
z-index: 2
|
z-index: 2
|
||||||
position: relative
|
position: relative
|
||||||
animation: float-up 1.2s ease-out
|
animation: float-up 1.2s ease-out
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
.users-panel,
|
.users-panel,
|
||||||
.chat-panel
|
.chat-panel
|
||||||
@ -131,7 +130,6 @@
|
|||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
padding: 15px
|
padding: 15px
|
||||||
min-height: 0
|
|
||||||
|
|
||||||
&::-webkit-scrollbar
|
&::-webkit-scrollbar
|
||||||
width: 6px
|
width: 6px
|
||||||
@ -224,7 +222,6 @@
|
|||||||
width: 70%
|
width: 70%
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
height: 100%
|
|
||||||
|
|
||||||
.chat-messages
|
.chat-messages
|
||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
@ -232,7 +229,6 @@
|
|||||||
padding: 15px
|
padding: 15px
|
||||||
display: flex
|
display: flex
|
||||||
flex-direction: column
|
flex-direction: column
|
||||||
min-height: 0
|
|
||||||
|
|
||||||
&::-webkit-scrollbar
|
&::-webkit-scrollbar
|
||||||
width: 8px
|
width: 8px
|
||||||
@ -334,362 +330,6 @@
|
|||||||
font-size: 1.4rem
|
font-size: 1.4rem
|
||||||
margin: 0
|
margin: 0
|
||||||
|
|
||||||
.playlist-section
|
|
||||||
background: rgba(20, 20, 30, 0.6)
|
|
||||||
backdrop-filter: blur(10px)
|
|
||||||
border-radius: 15px
|
|
||||||
padding: 25px
|
|
||||||
animation: float-up 0.8s ease-out
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
transition: all 0.3s ease
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color: rgba(255, 255, 255, 0.2)
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
.section-header
|
|
||||||
background: rgba(30, 30, 40, 0.95)
|
|
||||||
padding: 20px
|
|
||||||
border-radius: 12px
|
|
||||||
margin: -25px -25px 25px -25px
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
text-align: center
|
|
||||||
|
|
||||||
h2
|
|
||||||
margin: 0
|
|
||||||
font-size: 24px
|
|
||||||
color: $white
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
justify-content: center
|
|
||||||
gap: 10px
|
|
||||||
|
|
||||||
&:before, &:after
|
|
||||||
content: ""
|
|
||||||
height: 1px
|
|
||||||
width: 50px
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent)
|
|
||||||
|
|
||||||
.vote-info
|
|
||||||
color: rgba(255, 255, 255, 0.6)
|
|
||||||
font-size: 14px
|
|
||||||
margin-top: 5px
|
|
||||||
|
|
||||||
.playlists-grid
|
|
||||||
display: grid
|
|
||||||
grid-template-columns: repeat(3, 1fr)
|
|
||||||
gap: 20px
|
|
||||||
padding: 5px
|
|
||||||
|
|
||||||
@media (max-width: 1200px)
|
|
||||||
grid-template-columns: repeat(2, 1fr)
|
|
||||||
|
|
||||||
@media (max-width: 768px)
|
|
||||||
grid-template-columns: 1fr
|
|
||||||
|
|
||||||
.playlist-card
|
|
||||||
background: rgba(255, 255, 255, 0.05)
|
|
||||||
border-radius: 12px
|
|
||||||
overflow: hidden
|
|
||||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
|
||||||
cursor: pointer
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
transform: translateY(-5px)
|
|
||||||
background: rgba(255, 255, 255, 0.08)
|
|
||||||
border-color: rgba(255, 255, 255, 0.2)
|
|
||||||
|
|
||||||
.playlist-overlay
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
&.selected
|
|
||||||
background: rgba($yellow, 0.1)
|
|
||||||
border-color: rgba($yellow, 0.3)
|
|
||||||
box-shadow: 0 0 30px rgba($yellow, 0.15)
|
|
||||||
|
|
||||||
.playlist-overlay
|
|
||||||
opacity: 1
|
|
||||||
background: rgba(0, 0, 0, 0.4)
|
|
||||||
|
|
||||||
.your-vote
|
|
||||||
transform: translateY(0)
|
|
||||||
opacity: 1
|
|
||||||
|
|
||||||
.playlist-info h3
|
|
||||||
color: $yellow
|
|
||||||
|
|
||||||
.playlist-thumbnail
|
|
||||||
position: relative
|
|
||||||
width: 100%
|
|
||||||
padding-top: 56.25%
|
|
||||||
|
|
||||||
img
|
|
||||||
position: absolute
|
|
||||||
top: 0
|
|
||||||
left: 0
|
|
||||||
width: 100%
|
|
||||||
height: 100%
|
|
||||||
object-fit: cover
|
|
||||||
|
|
||||||
.playlist-overlay
|
|
||||||
position: absolute
|
|
||||||
inset: 0
|
|
||||||
background: rgba(0, 0, 0, 0.3)
|
|
||||||
opacity: 0
|
|
||||||
transition: all 0.3s ease
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
justify-content: center
|
|
||||||
align-items: center
|
|
||||||
|
|
||||||
.vote-count
|
|
||||||
background: rgba(20, 20, 30, 0.85)
|
|
||||||
padding: 12px 20px
|
|
||||||
border-radius: 15px
|
|
||||||
text-align: center
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
|
||||||
backdrop-filter: blur(5px)
|
|
||||||
|
|
||||||
.count
|
|
||||||
font-size: 28px
|
|
||||||
font-weight: bold
|
|
||||||
color: $yellow
|
|
||||||
display: block
|
|
||||||
text-shadow: 0 0 10px rgba($yellow, 0.5)
|
|
||||||
|
|
||||||
.vote-label
|
|
||||||
font-size: 12px
|
|
||||||
color: rgba(255, 255, 255, 0.7)
|
|
||||||
|
|
||||||
.your-vote
|
|
||||||
position: absolute
|
|
||||||
bottom: 15px
|
|
||||||
background: $yellow
|
|
||||||
color: rgba(0, 0, 0, 0.8)
|
|
||||||
padding: 8px 16px
|
|
||||||
border-radius: 20px
|
|
||||||
font-weight: bold
|
|
||||||
font-size: 12px
|
|
||||||
transform: translateY(20px)
|
|
||||||
opacity: 0
|
|
||||||
transition: all 0.3s ease
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
.playlist-info
|
|
||||||
padding: 15px
|
|
||||||
background: rgba(20, 20, 30, 0.4)
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.05)
|
|
||||||
|
|
||||||
h3
|
|
||||||
color: $white
|
|
||||||
margin-bottom: 8px
|
|
||||||
font-size: 18px
|
|
||||||
transition: color 0.3s ease
|
|
||||||
|
|
||||||
.playlist-details
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
gap: 8px
|
|
||||||
|
|
||||||
span
|
|
||||||
color: rgba(255, 255, 255, 0.6)
|
|
||||||
font-size: 13px
|
|
||||||
|
|
||||||
.vote-percentage-bar
|
|
||||||
height: 4px
|
|
||||||
background: rgba(255, 255, 255, 0.1)
|
|
||||||
border-radius: 2px
|
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
.fill
|
|
||||||
height: 100%
|
|
||||||
background: linear-gradient(90deg, $yellow, rgba($yellow, 0.5))
|
|
||||||
box-shadow: 0 0 10px rgba($yellow, 0.3)
|
|
||||||
transition: width 0.3s ease
|
|
||||||
|
|
||||||
.bottom-section
|
|
||||||
display: flex
|
|
||||||
gap: 30px
|
|
||||||
width: 100%
|
|
||||||
min-height: 0
|
|
||||||
height: 300px
|
|
||||||
margin-top: 20px
|
|
||||||
|
|
||||||
.users-panel
|
|
||||||
height: 100%
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
background: rgba(20, 20, 30, 0.6)
|
|
||||||
border-radius: 15px
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
overflow: hidden
|
|
||||||
transition: all 0.3s ease
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color: rgba(255, 255, 255, 0.2)
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
.panel-header
|
|
||||||
background: rgba(30, 30, 40, 0.95)
|
|
||||||
padding: 20px
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
|
|
||||||
h2
|
|
||||||
font-size: 20px
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
gap: 10px
|
|
||||||
|
|
||||||
svg
|
|
||||||
color: $yellow
|
|
||||||
filter: drop-shadow(0 0 8px rgba($yellow, 0.4))
|
|
||||||
|
|
||||||
.users-list
|
|
||||||
padding: 15px
|
|
||||||
flex: 1
|
|
||||||
overflow-y: auto
|
|
||||||
min-height: 0
|
|
||||||
|
|
||||||
.user-item
|
|
||||||
background: rgba(255, 255, 255, 0.05)
|
|
||||||
margin-bottom: 10px
|
|
||||||
padding: 12px 15px
|
|
||||||
border-radius: 10px
|
|
||||||
transition: all 0.3s ease
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
transform: translateY(-2px)
|
|
||||||
background: rgba(255, 255, 255, 0.08)
|
|
||||||
border-color: rgba(255, 255, 255, 0.1)
|
|
||||||
|
|
||||||
&.host
|
|
||||||
background: rgba($yellow, 0.1)
|
|
||||||
border-color: rgba($yellow, 0.3)
|
|
||||||
|
|
||||||
.host-badge
|
|
||||||
background: $yellow
|
|
||||||
color: #000
|
|
||||||
font-size: 12px
|
|
||||||
padding: 4px 8px
|
|
||||||
border-radius: 20px
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
.game-controls
|
|
||||||
padding: 15px
|
|
||||||
background: rgba(20, 20, 30, 0.4)
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.05)
|
|
||||||
|
|
||||||
.start-game-button
|
|
||||||
width: 100%
|
|
||||||
padding: 12px
|
|
||||||
background: linear-gradient(135deg, $purple, $blue)
|
|
||||||
border-radius: 10px
|
|
||||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
|
||||||
|
|
||||||
&:not(.disabled):hover
|
|
||||||
transform: translateY(-2px)
|
|
||||||
filter: brightness(1.1)
|
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4)
|
|
||||||
|
|
||||||
.chat-panel
|
|
||||||
flex: 1
|
|
||||||
height: 100%
|
|
||||||
background: rgba(20, 20, 30, 0.6)
|
|
||||||
border-radius: 15px
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
overflow: hidden
|
|
||||||
transition: all 0.3s ease
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
border-color: rgba(255, 255, 255, 0.2)
|
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
.panel-header
|
|
||||||
background: rgba(30, 30, 40, 0.95)
|
|
||||||
padding: 20px
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
|
|
||||||
h2
|
|
||||||
font-size: 20px
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
gap: 10px
|
|
||||||
|
|
||||||
svg
|
|
||||||
color: $blue
|
|
||||||
filter: drop-shadow(0 0 8px rgba($blue, 0.4))
|
|
||||||
|
|
||||||
.chat-messages
|
|
||||||
flex: 1
|
|
||||||
padding: 20px
|
|
||||||
overflow-y: auto
|
|
||||||
display: flex
|
|
||||||
flex-direction: column
|
|
||||||
gap: 10px
|
|
||||||
min-height: 0
|
|
||||||
|
|
||||||
.message
|
|
||||||
max-width: 85%
|
|
||||||
padding: 10px 15px
|
|
||||||
border-radius: 12px
|
|
||||||
background: rgba(255, 255, 255, 0.05)
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.05)
|
|
||||||
transition: all 0.3s ease
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
background: rgba(255, 255, 255, 0.08)
|
|
||||||
border-color: rgba(255, 255, 255, 0.1)
|
|
||||||
transform: translateY(-1px)
|
|
||||||
|
|
||||||
.message-sender
|
|
||||||
color: $yellow
|
|
||||||
font-weight: 600
|
|
||||||
margin-right: 8px
|
|
||||||
|
|
||||||
&.system-message
|
|
||||||
align-self: center
|
|
||||||
background: rgba(0, 0, 0, 0.2)
|
|
||||||
font-style: italic
|
|
||||||
color: rgba(255, 255, 255, 0.6)
|
|
||||||
padding: 8px 16px
|
|
||||||
border-radius: 20px
|
|
||||||
|
|
||||||
.chat-input
|
|
||||||
padding: 15px
|
|
||||||
background: rgba(20, 20, 30, 0.4)
|
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.05)
|
|
||||||
display: flex
|
|
||||||
gap: 10px
|
|
||||||
|
|
||||||
input
|
|
||||||
flex: 1
|
|
||||||
background: rgba(0, 0, 0, 0.2)
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
|
||||||
border-radius: 8px
|
|
||||||
padding: 12px 15px
|
|
||||||
color: $white
|
|
||||||
transition: all 0.3s ease
|
|
||||||
|
|
||||||
&:focus
|
|
||||||
border-color: rgba($blue, 0.5)
|
|
||||||
box-shadow: 0 0 15px rgba($blue, 0.1)
|
|
||||||
background: rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
button
|
|
||||||
padding: 12px 20px
|
|
||||||
background: linear-gradient(135deg, $purple, $blue)
|
|
||||||
border-radius: 8px
|
|
||||||
transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
|
|
||||||
|
|
||||||
&:hover
|
|
||||||
transform: translateY(-2px)
|
|
||||||
filter: brightness(1.1)
|
|
||||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3)
|
|
||||||
|
|
||||||
@keyframes pop
|
@keyframes pop
|
||||||
0%
|
0%
|
||||||
transform: scale(1)
|
transform: scale(1)
|
||||||
|
@ -13,7 +13,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"googleapis": "^146.0.0",
|
|
||||||
"socket.io": "^4.8.1"
|
"socket.io": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
209
pnpm-lock.yaml
generated
209
pnpm-lock.yaml
generated
@ -11,9 +11,6 @@ importers:
|
|||||||
express:
|
express:
|
||||||
specifier: ^4.21.2
|
specifier: ^4.21.2
|
||||||
version: 4.21.2
|
version: 4.21.2
|
||||||
googleapis:
|
|
||||||
specifier: ^146.0.0
|
|
||||||
version: 146.0.0
|
|
||||||
socket.io:
|
socket.io:
|
||||||
specifier: ^4.8.1
|
specifier: ^4.8.1
|
||||||
version: 4.8.1
|
version: 4.8.1
|
||||||
@ -40,10 +37,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
agent-base@7.1.3:
|
|
||||||
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
|
||||||
engines: {node: '>= 14'}
|
|
||||||
|
|
||||||
ansi-regex@5.0.1:
|
ansi-regex@5.0.1:
|
||||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -62,16 +55,10 @@ packages:
|
|||||||
balanced-match@1.0.2:
|
balanced-match@1.0.2:
|
||||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||||
|
|
||||||
base64-js@1.5.1:
|
|
||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
|
||||||
|
|
||||||
base64id@2.0.0:
|
base64id@2.0.0:
|
||||||
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
|
||||||
engines: {node: ^4.5.0 || >= 5.9}
|
engines: {node: ^4.5.0 || >= 5.9}
|
||||||
|
|
||||||
bignumber.js@9.1.2:
|
|
||||||
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
|
|
||||||
|
|
||||||
binary-extensions@2.3.0:
|
binary-extensions@2.3.0:
|
||||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -87,9 +74,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1:
|
|
||||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
|
||||||
|
|
||||||
bytes@3.1.2:
|
bytes@3.1.2:
|
||||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -181,9 +165,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
|
||||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||||
|
|
||||||
@ -233,9 +214,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
|
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
|
||||||
extend@3.0.2:
|
|
||||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -260,14 +238,6 @@ packages:
|
|||||||
function-bind@1.1.2:
|
function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
gaxios@6.7.1:
|
|
||||||
resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
gcp-metadata@6.1.1:
|
|
||||||
resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
get-caller-file@2.0.5:
|
get-caller-file@2.0.5:
|
||||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
@ -284,30 +254,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
google-auth-library@9.15.1:
|
|
||||||
resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
google-logging-utils@0.0.2:
|
|
||||||
resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
googleapis-common@7.2.0:
|
|
||||||
resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
googleapis@146.0.0:
|
|
||||||
resolution: {integrity: sha512-NewqvhnBZOJsugCAOo636O0BGE/xY7Cg/v8Rjm1+5LkJCjcqAzLleJ6igd5vrRExJLSKrY9uHy9iKE7r0PrfhQ==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
gopd@1.2.0:
|
gopd@1.2.0:
|
||||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
gtoken@7.1.0:
|
|
||||||
resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
|
|
||||||
has-flag@3.0.0:
|
has-flag@3.0.0:
|
||||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@ -328,10 +278,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
https-proxy-agent@7.0.6:
|
|
||||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
|
||||||
engines: {node: '>= 14'}
|
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -366,19 +312,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
is-stream@2.0.1:
|
|
||||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
json-bigint@1.0.0:
|
|
||||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
|
||||||
|
|
||||||
jwa@2.0.0:
|
|
||||||
resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==}
|
|
||||||
|
|
||||||
jws@4.0.0:
|
|
||||||
resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==}
|
|
||||||
|
|
||||||
lodash@4.17.21:
|
lodash@4.17.21:
|
||||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
@ -423,15 +356,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
|
||||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
|
||||||
engines: {node: 4.x || >=6.0.0}
|
|
||||||
peerDependencies:
|
|
||||||
encoding: ^0.1.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
encoding:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
nodemon@3.1.9:
|
nodemon@3.1.9:
|
||||||
resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==}
|
resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -587,9 +511,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
tr46@0.0.3:
|
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
|
||||||
|
|
||||||
tree-kill@1.2.2:
|
tree-kill@1.2.2:
|
||||||
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -611,27 +532,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
url-template@2.0.8:
|
|
||||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
|
||||||
|
|
||||||
utils-merge@1.0.1:
|
utils-merge@1.0.1:
|
||||||
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
|
||||||
engines: {node: '>= 0.4.0'}
|
engines: {node: '>= 0.4.0'}
|
||||||
|
|
||||||
uuid@9.0.1:
|
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
vary@1.1.2:
|
vary@1.1.2:
|
||||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
webidl-conversions@3.0.1:
|
|
||||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
|
||||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -677,8 +585,6 @@ snapshots:
|
|||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
negotiator: 0.6.3
|
negotiator: 0.6.3
|
||||||
|
|
||||||
agent-base@7.1.3: {}
|
|
||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
||||||
ansi-styles@4.3.0:
|
ansi-styles@4.3.0:
|
||||||
@ -694,12 +600,8 @@ snapshots:
|
|||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
|
|
||||||
base64-js@1.5.1: {}
|
|
||||||
|
|
||||||
base64id@2.0.0: {}
|
base64id@2.0.0: {}
|
||||||
|
|
||||||
bignumber.js@9.1.2: {}
|
|
||||||
|
|
||||||
binary-extensions@2.3.0: {}
|
binary-extensions@2.3.0: {}
|
||||||
|
|
||||||
body-parser@1.20.3:
|
body-parser@1.20.3:
|
||||||
@ -728,8 +630,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fill-range: 7.1.1
|
fill-range: 7.1.1
|
||||||
|
|
||||||
buffer-equal-constant-time@1.0.1: {}
|
|
||||||
|
|
||||||
bytes@3.1.2: {}
|
bytes@3.1.2: {}
|
||||||
|
|
||||||
call-bind-apply-helpers@1.0.2:
|
call-bind-apply-helpers@1.0.2:
|
||||||
@ -820,10 +720,6 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
gopd: 1.2.0
|
gopd: 1.2.0
|
||||||
|
|
||||||
ecdsa-sig-formatter@1.0.11:
|
|
||||||
dependencies:
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@ -900,8 +796,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
extend@3.0.2: {}
|
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
@ -927,26 +821,6 @@ snapshots:
|
|||||||
|
|
||||||
function-bind@1.1.2: {}
|
function-bind@1.1.2: {}
|
||||||
|
|
||||||
gaxios@6.7.1:
|
|
||||||
dependencies:
|
|
||||||
extend: 3.0.2
|
|
||||||
https-proxy-agent: 7.0.6
|
|
||||||
is-stream: 2.0.1
|
|
||||||
node-fetch: 2.7.0
|
|
||||||
uuid: 9.0.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
gcp-metadata@6.1.1:
|
|
||||||
dependencies:
|
|
||||||
gaxios: 6.7.1
|
|
||||||
google-logging-utils: 0.0.2
|
|
||||||
json-bigint: 1.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
get-caller-file@2.0.5: {}
|
get-caller-file@2.0.5: {}
|
||||||
|
|
||||||
get-intrinsic@1.3.0:
|
get-intrinsic@1.3.0:
|
||||||
@ -971,50 +845,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
|
|
||||||
google-auth-library@9.15.1:
|
|
||||||
dependencies:
|
|
||||||
base64-js: 1.5.1
|
|
||||||
ecdsa-sig-formatter: 1.0.11
|
|
||||||
gaxios: 6.7.1
|
|
||||||
gcp-metadata: 6.1.1
|
|
||||||
gtoken: 7.1.0
|
|
||||||
jws: 4.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
google-logging-utils@0.0.2: {}
|
|
||||||
|
|
||||||
googleapis-common@7.2.0:
|
|
||||||
dependencies:
|
|
||||||
extend: 3.0.2
|
|
||||||
gaxios: 6.7.1
|
|
||||||
google-auth-library: 9.15.1
|
|
||||||
qs: 6.13.0
|
|
||||||
url-template: 2.0.8
|
|
||||||
uuid: 9.0.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
googleapis@146.0.0:
|
|
||||||
dependencies:
|
|
||||||
google-auth-library: 9.15.1
|
|
||||||
googleapis-common: 7.2.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
gopd@1.2.0: {}
|
gopd@1.2.0: {}
|
||||||
|
|
||||||
gtoken@7.1.0:
|
|
||||||
dependencies:
|
|
||||||
gaxios: 6.7.1
|
|
||||||
jws: 4.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
has-flag@3.0.0: {}
|
has-flag@3.0.0: {}
|
||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
@ -1033,13 +865,6 @@ snapshots:
|
|||||||
statuses: 2.0.1
|
statuses: 2.0.1
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
https-proxy-agent@7.0.6:
|
|
||||||
dependencies:
|
|
||||||
agent-base: 7.1.3
|
|
||||||
debug: 4.3.7(supports-color@5.5.0)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
iconv-lite@0.4.24:
|
iconv-lite@0.4.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
@ -1064,23 +889,6 @@ snapshots:
|
|||||||
|
|
||||||
is-number@7.0.0: {}
|
is-number@7.0.0: {}
|
||||||
|
|
||||||
is-stream@2.0.1: {}
|
|
||||||
|
|
||||||
json-bigint@1.0.0:
|
|
||||||
dependencies:
|
|
||||||
bignumber.js: 9.1.2
|
|
||||||
|
|
||||||
jwa@2.0.0:
|
|
||||||
dependencies:
|
|
||||||
buffer-equal-constant-time: 1.0.1
|
|
||||||
ecdsa-sig-formatter: 1.0.11
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
jws@4.0.0:
|
|
||||||
dependencies:
|
|
||||||
jwa: 2.0.0
|
|
||||||
safe-buffer: 5.2.1
|
|
||||||
|
|
||||||
lodash@4.17.21: {}
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
@ -1109,10 +917,6 @@ snapshots:
|
|||||||
|
|
||||||
negotiator@0.6.3: {}
|
negotiator@0.6.3: {}
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
|
||||||
dependencies:
|
|
||||||
whatwg-url: 5.0.0
|
|
||||||
|
|
||||||
nodemon@3.1.9:
|
nodemon@3.1.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 3.6.0
|
chokidar: 3.6.0
|
||||||
@ -1303,8 +1107,6 @@ snapshots:
|
|||||||
|
|
||||||
touch@3.1.1: {}
|
touch@3.1.1: {}
|
||||||
|
|
||||||
tr46@0.0.3: {}
|
|
||||||
|
|
||||||
tree-kill@1.2.2: {}
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
@ -1320,21 +1122,10 @@ snapshots:
|
|||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
url-template@2.0.8: {}
|
|
||||||
|
|
||||||
utils-merge@1.0.1: {}
|
utils-merge@1.0.1: {}
|
||||||
|
|
||||||
uuid@9.0.1: {}
|
|
||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
webidl-conversions@3.0.1: {}
|
|
||||||
|
|
||||||
whatwg-url@5.0.0:
|
|
||||||
dependencies:
|
|
||||||
tr46: 0.0.3
|
|
||||||
webidl-conversions: 3.0.1
|
|
||||||
|
|
||||||
wrap-ansi@7.0.0:
|
wrap-ansi@7.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
|
@ -14,9 +14,6 @@ const gameStates = {};
|
|||||||
|
|
||||||
const initializeGameState = (roomId) => {
|
const initializeGameState = (roomId) => {
|
||||||
if (!gameStates[roomId]) {
|
if (!gameStates[roomId]) {
|
||||||
const selectedPlaylist = roomController.getWinningPlaylist(roomId);
|
|
||||||
console.log(`Initializing game with winning playlist: ${selectedPlaylist}`);
|
|
||||||
|
|
||||||
gameStates[roomId] = {
|
gameStates[roomId] = {
|
||||||
round: 0,
|
round: 0,
|
||||||
phase: 'waiting',
|
phase: 'waiting',
|
||||||
@ -32,7 +29,6 @@ const initializeGameState = (roomId) => {
|
|||||||
guessing: 10
|
guessing: 10
|
||||||
},
|
},
|
||||||
lastFrequency: 440,
|
lastFrequency: 440,
|
||||||
selectedPlaylist: selectedPlaylist,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const users = roomController.getRoomUsers(roomId);
|
const users = roomController.getRoomUsers(roomId);
|
||||||
@ -44,8 +40,6 @@ const initializeGameState = (roomId) => {
|
|||||||
return gameStates[roomId];
|
return gameStates[roomId];
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_ROUNDS = 5;
|
|
||||||
|
|
||||||
const startNewRound = async (roomId) => {
|
const startNewRound = async (roomId) => {
|
||||||
const gameState = gameStates[roomId];
|
const gameState = gameStates[roomId];
|
||||||
if (!gameState) return false;
|
if (!gameState) return false;
|
||||||
@ -54,11 +48,6 @@ const startNewRound = async (roomId) => {
|
|||||||
if (users.length < 2) return false;
|
if (users.length < 2) return false;
|
||||||
|
|
||||||
gameState.round += 1;
|
gameState.round += 1;
|
||||||
|
|
||||||
if (gameState.round > MAX_ROUNDS) {
|
|
||||||
return { gameEnd: true, finalScores: gameState.scores };
|
|
||||||
}
|
|
||||||
|
|
||||||
gameState.phase = 'composing';
|
gameState.phase = 'composing';
|
||||||
gameState.guessResults = {};
|
gameState.guessResults = {};
|
||||||
gameState.roundStartTime = Date.now();
|
gameState.roundStartTime = Date.now();
|
||||||
@ -90,10 +79,10 @@ const determineNextComposer = (roomId, gameState, users) => {
|
|||||||
return users[(currentIndex + 1) % users.length].id;
|
return users[(currentIndex + 1) % users.length].id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Then modify the selectSongAndOptions function to use the shuffle:
|
||||||
const selectSongAndOptions = async (gameState) => {
|
const selectSongAndOptions = async (gameState) => {
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching songs from playlist: ${gameState.selectedPlaylist}`);
|
const availableIds = await youtubeService.getAvailableSongIds();
|
||||||
const availableIds = await youtubeService.getAvailableSongIds(gameState.selectedPlaylist);
|
|
||||||
|
|
||||||
if (!availableIds || availableIds.length === 0) {
|
if (!availableIds || availableIds.length === 0) {
|
||||||
console.error("No song IDs available for selection");
|
console.error("No song IDs available for selection");
|
||||||
@ -113,6 +102,7 @@ const selectSongAndOptions = async (gameState) => {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shuffle the song options to randomize the position of the correct answer
|
||||||
gameState.songOptions = shuffleArray(Array.from(optionIds).map(id => ({ id })));
|
gameState.songOptions = shuffleArray(Array.from(optionIds).map(id => ({ id })));
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -224,27 +214,6 @@ const getGameState = (roomId) => {
|
|||||||
return gameStates[roomId] || null;
|
return gameStates[roomId] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFinalScores = (roomId) => {
|
|
||||||
const gameState = gameStates[roomId];
|
|
||||||
if (!gameState) return null;
|
|
||||||
|
|
||||||
const users = roomController.getRoomUsers(roomId);
|
|
||||||
const finalScores = {};
|
|
||||||
|
|
||||||
Object.entries(gameState.scores).forEach(([userId, score]) => {
|
|
||||||
const user = users.find(u => u.id === userId);
|
|
||||||
finalScores[userId] = {
|
|
||||||
score: score,
|
|
||||||
name: user?.name || "Player"
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
scores: finalScores,
|
|
||||||
lastRound: gameState.round
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
initializeGameState,
|
initializeGameState,
|
||||||
startNewRound,
|
startNewRound,
|
||||||
@ -260,7 +229,5 @@ module.exports = {
|
|||||||
getSelectedSong,
|
getSelectedSong,
|
||||||
cleanupGameState,
|
cleanupGameState,
|
||||||
getCurrentComposer,
|
getCurrentComposer,
|
||||||
getGameState,
|
getGameState
|
||||||
getFinalScores,
|
|
||||||
MAX_ROUNDS
|
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
const youtubeService = require('../services/youtubeService');
|
|
||||||
|
|
||||||
let cleanupGameState;
|
let cleanupGameState;
|
||||||
|
|
||||||
const setCleanupGameState = (cleanupFunction) => {
|
const setCleanupGameState = (cleanupFunction) => {
|
||||||
@ -12,41 +10,19 @@ module.exports.roomExists = (roomId) => rooms[roomId] !== undefined;
|
|||||||
|
|
||||||
module.exports.isRoomOpen = (roomId) => rooms[roomId] && rooms[roomId].state === 'waiting';
|
module.exports.isRoomOpen = (roomId) => rooms[roomId] && rooms[roomId].state === 'waiting';
|
||||||
|
|
||||||
const initializeRoom = async (roomId, user) => {
|
module.exports.connectUserToRoom = (roomId, user) => {
|
||||||
try {
|
|
||||||
const randomPlaylists = await youtubeService.getRandomPlaylists(3);
|
|
||||||
rooms[roomId] = {
|
|
||||||
members: [{...user, creator: true}],
|
|
||||||
settings: {},
|
|
||||||
state: 'waiting',
|
|
||||||
playlistVotes: {},
|
|
||||||
selectedPlaylist: null,
|
|
||||||
availablePlaylists: randomPlaylists
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error initializing room playlists:", error);
|
|
||||||
rooms[roomId] = {
|
|
||||||
members: [{...user, creator: true}],
|
|
||||||
settings: {},
|
|
||||||
state: 'waiting',
|
|
||||||
playlistVotes: {},
|
|
||||||
selectedPlaylist: null,
|
|
||||||
availablePlaylists: {
|
|
||||||
seventies: youtubeService.PLAYLISTS.seventies
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.connectUserToRoom = async (roomId, user) => {
|
|
||||||
roomId = roomId.toUpperCase();
|
roomId = roomId.toUpperCase();
|
||||||
if (rooms[roomId]) {
|
if (rooms[roomId]) {
|
||||||
rooms[roomId].members.push({...user, creator: false});
|
rooms[roomId].members.push({...user, creator: false});
|
||||||
} else {
|
} else {
|
||||||
await initializeRoom(roomId, user);
|
rooms[roomId] = {
|
||||||
|
members: [{...user, creator: true}],
|
||||||
|
settings: {},
|
||||||
|
state: 'waiting'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
console.log(`User ${user.name} connected to room ${roomId}`);
|
console.log(`User ${user.name} connected to room ${roomId}`);
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports.getUserRoom = (userId) => {
|
module.exports.getUserRoom = (userId) => {
|
||||||
for (const roomId in rooms) {
|
for (const roomId in rooms) {
|
||||||
@ -106,6 +82,7 @@ module.exports.disconnectUser = (userId) => {
|
|||||||
|
|
||||||
if (memberIndex !== -1) {
|
if (memberIndex !== -1) {
|
||||||
if (room.members[memberIndex].creator && room.members.length > 1) {
|
if (room.members[memberIndex].creator && room.members.length > 1) {
|
||||||
|
// Transfer host status to the next user
|
||||||
room.members[1].creator = true;
|
room.members[1].creator = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,73 +133,3 @@ module.exports.getUserName = (userId) => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.voteForPlaylist = (roomId, userId, playlistId) => {
|
|
||||||
if (!rooms[roomId]) return false;
|
|
||||||
|
|
||||||
const room = rooms[roomId];
|
|
||||||
if (room.state !== 'waiting') return false;
|
|
||||||
|
|
||||||
if (!room.playlistVotes) {
|
|
||||||
room.playlistVotes = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(room.playlistVotes).forEach(([pid, voters]) => {
|
|
||||||
room.playlistVotes[pid] = voters.filter(id => id !== userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!room.playlistVotes[playlistId]) {
|
|
||||||
room.playlistVotes[playlistId] = [];
|
|
||||||
}
|
|
||||||
room.playlistVotes[playlistId].push(userId);
|
|
||||||
|
|
||||||
console.log(`Updated votes for room ${roomId}:`, room.playlistVotes);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.getPlaylistVotes = (roomId) => {
|
|
||||||
return rooms[roomId]?.playlistVotes || {};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.getWinningPlaylist = (roomId) => {
|
|
||||||
const room = rooms[roomId];
|
|
||||||
if (!room || !room.playlistVotes) {
|
|
||||||
console.log(`No votes found for room ${roomId}, using default playlist`);
|
|
||||||
return room.availablePlaylists[Object.keys(room.availablePlaylists)[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
let maxVotes = 0;
|
|
||||||
let winningPlaylist = null;
|
|
||||||
|
|
||||||
console.log(`Calculating winning playlist for room ${roomId}`);
|
|
||||||
console.log('Current votes:', room.playlistVotes);
|
|
||||||
|
|
||||||
Object.entries(room.playlistVotes).forEach(([playlistId, voters]) => {
|
|
||||||
console.log(`Playlist ${playlistId} has ${voters.length} votes`);
|
|
||||||
if (voters.length > maxVotes) {
|
|
||||||
maxVotes = voters.length;
|
|
||||||
winningPlaylist = playlistId;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!winningPlaylist) {
|
|
||||||
console.log('No winning playlist found, using first available');
|
|
||||||
winningPlaylist = room.availablePlaylists[Object.keys(room.availablePlaylists)[0]];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Selected winning playlist: ${winningPlaylist}`);
|
|
||||||
return winningPlaylist;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.getAvailablePlaylists = (roomId) => {
|
|
||||||
return rooms[roomId]?.availablePlaylists || {};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.updateAvailablePlaylists = (roomId, newPlaylists) => {
|
|
||||||
if (rooms[roomId]) {
|
|
||||||
rooms[roomId].availablePlaylists = newPlaylists;
|
|
||||||
rooms[roomId].playlistVotes = {};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
@ -109,23 +109,6 @@ module.exports = (io) => (socket) => {
|
|||||||
io.to(roomId).emit('round-results', results);
|
io.to(roomId).emit('round-results', results);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNextRound = async (roomId) => {
|
|
||||||
try {
|
|
||||||
const result = await gameController.startNewRound(roomId);
|
|
||||||
|
|
||||||
if (result.gameEnd) {
|
|
||||||
const finalData = gameController.getFinalScores(roomId);
|
|
||||||
io.to(roomId).emit('game-ended', finalData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRoundStart(roomId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error starting next round:", error);
|
|
||||||
socket.emit("error", { message: "Failed to start next round due to an error" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
const roomId = roomController.getUserRoom(socket.id);
|
const roomId = roomController.getUserRoom(socket.id);
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
@ -134,7 +117,7 @@ module.exports = (io) => (socket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("join-room", async ({roomId, name}) => {
|
socket.on("join-room", ({roomId, name}) => {
|
||||||
if (currentRoomId) return socket.emit("already-in-room", currentRoomId);
|
if (currentRoomId) return socket.emit("already-in-room", currentRoomId);
|
||||||
|
|
||||||
roomId = roomId.toString().toUpperCase();
|
roomId = roomId.toString().toUpperCase();
|
||||||
@ -145,35 +128,14 @@ module.exports = (io) => (socket) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentUser = {id: socket.id, name: name.toString()};
|
currentUser = {id: socket.id, name: name.toString()};
|
||||||
await roomController.connectUserToRoom(roomId, currentUser);
|
roomController.connectUserToRoom(roomId, currentUser);
|
||||||
socket.join(roomId);
|
socket.join(roomId);
|
||||||
|
|
||||||
const users = roomController.getRoomUsers(roomId);
|
const users = roomController.getRoomUsers(roomId);
|
||||||
const votes = roomController.getPlaylistVotes(roomId);
|
io.to(roomId).emit("room-users-update", users);
|
||||||
const availablePlaylists = roomController.getAvailablePlaylists(roomId);
|
socket.to(roomId).emit("user-connected", currentUser);
|
||||||
|
|
||||||
socket.emit("room-joined", roomId);
|
socket.emit("room-joined", roomId);
|
||||||
socket.emit("room-users", users);
|
|
||||||
socket.emit("playlist-votes-updated", votes);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const details = await youtubeService.getPlaylistDetails(availablePlaylists);
|
|
||||||
Object.keys(details).forEach(genre => {
|
|
||||||
const playlistId = details[genre].id;
|
|
||||||
details[genre].votes = votes[playlistId]?.length || 0;
|
|
||||||
});
|
|
||||||
socket.emit("playlist-options", details);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error sending playlist details to new user:", error);
|
|
||||||
socket.emit("error", {
|
|
||||||
message: "Failed to load playlists",
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.to(roomId).emit("user-connected", currentUser);
|
|
||||||
io.to(roomId).emit("room-users-update", users);
|
|
||||||
|
|
||||||
currentRoomId = roomId;
|
currentRoomId = roomId;
|
||||||
} else {
|
} else {
|
||||||
socket.emit("room-not-found", roomId);
|
socket.emit("room-not-found", roomId);
|
||||||
@ -315,7 +277,17 @@ module.exports = (io) => (socket) => {
|
|||||||
return socket.emit("error", { message: "At least 2 players are required" });
|
return socket.emit("error", { message: "At least 2 players are required" });
|
||||||
}
|
}
|
||||||
|
|
||||||
await handleNextRound(roomId);
|
try {
|
||||||
|
const success = await gameController.startNewRound(roomId);
|
||||||
|
if (success) {
|
||||||
|
handleRoundStart(roomId);
|
||||||
|
} else {
|
||||||
|
socket.emit("error", { message: "Failed to start next round" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error starting next round:", error);
|
||||||
|
socket.emit("error", { message: "Failed to start next round due to an error" });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("get-current-frequency", () => {
|
socket.on("get-current-frequency", () => {
|
||||||
@ -329,58 +301,11 @@ module.exports = (io) => (socket) => {
|
|||||||
|
|
||||||
socket.on("get-playlist-songs", async () => {
|
socket.on("get-playlist-songs", async () => {
|
||||||
try {
|
try {
|
||||||
const roomId = roomController.getUserRoom(socket.id);
|
const songs = await youtubeService.fetchPlaylistSongs();
|
||||||
if (!roomId) {
|
|
||||||
throw new Error("User not in a room");
|
|
||||||
}
|
|
||||||
|
|
||||||
const gameState = gameController.getGameState(roomId);
|
|
||||||
const playlistId = gameState?.selectedPlaylist || null;
|
|
||||||
|
|
||||||
console.log(`Fetching songs for playlist ${playlistId}`);
|
|
||||||
const songs = await youtubeService.fetchPlaylistSongs(playlistId);
|
|
||||||
socket.emit("playlist-songs", { songs });
|
socket.emit("playlist-songs", { songs });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error sending playlist songs:", error);
|
console.error("Error sending playlist songs:", error);
|
||||||
socket.emit("playlist-songs", { songs: [] });
|
socket.emit("playlist-songs", { songs: youtubeService.getDefaultSongs() });
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("get-playlist-options", async () => {
|
|
||||||
try {
|
|
||||||
const roomId = roomController.getUserRoom(socket.id);
|
|
||||||
if (!roomId) {
|
|
||||||
throw new Error("User not in a room");
|
|
||||||
}
|
|
||||||
|
|
||||||
const availablePlaylists = roomController.getAvailablePlaylists(roomId);
|
|
||||||
const details = await youtubeService.getPlaylistDetails(availablePlaylists);
|
|
||||||
|
|
||||||
if (Object.keys(details).length === 0) {
|
|
||||||
const newPlaylists = await youtubeService.getRandomPlaylists(3);
|
|
||||||
const newDetails = await youtubeService.getPlaylistDetails(newPlaylists);
|
|
||||||
|
|
||||||
roomController.updateAvailablePlaylists(roomId, newPlaylists);
|
|
||||||
socket.emit("playlist-options", newDetails);
|
|
||||||
} else {
|
|
||||||
socket.emit("playlist-options", details);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching playlist options:", error);
|
|
||||||
socket.emit("error", {
|
|
||||||
message: "Failed to load playlists",
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on("vote-playlist", ({ playlistId }) => {
|
|
||||||
const roomId = roomController.getUserRoom(socket.id);
|
|
||||||
if (!roomId) return;
|
|
||||||
|
|
||||||
if (roomController.voteForPlaylist(roomId, socket.id, playlistId)) {
|
|
||||||
const votes = roomController.getPlaylistVotes(roomId);
|
|
||||||
io.to(roomId).emit("playlist-votes-updated", votes);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
@ -21,14 +21,8 @@ const roomController = require('./controller/room');
|
|||||||
const gameController = require('./controller/game');
|
const gameController = require('./controller/game');
|
||||||
roomController.setCleanupGameState(gameController.cleanupGameState);
|
roomController.setCleanupGameState(gameController.cleanupGameState);
|
||||||
|
|
||||||
io.on("connection", (socket) => {
|
// Handle socket connections
|
||||||
try {
|
io.on("connection", require("./handler/connection")(io));
|
||||||
require("./handler/connection")(io)(socket);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error handling socket connection:', error);
|
|
||||||
socket.emit('error', { message: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.on('error', (error) => {
|
server.on('error', (error) => {
|
||||||
console.error('Server error:', error);
|
console.error('Server error:', error);
|
||||||
|
@ -1,94 +1,23 @@
|
|||||||
const { google } = require('googleapis');
|
|
||||||
const youtube = google.youtube('v3');
|
|
||||||
|
|
||||||
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
|
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
|
||||||
|
const PLAYLIST_ID = "PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf";
|
||||||
|
|
||||||
const PLAYLISTS = {
|
let cachedSongs = null;
|
||||||
seventies: 'PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf',
|
let lastFetchTime = 0;
|
||||||
eighties: 'PLmXxqSJJq-yUvMWKuZQAB_8yxnjZaOZUp',
|
|
||||||
nineties: 'PLmXxqSJJq-yUF3jbzjF_pa--kuBuMlyQQ',
|
|
||||||
pop: 'PLxA687tYuMWhkqYjvAGtW_heiEL4Hk_Lx',
|
|
||||||
dance: 'PL64E6BD94546734D8'
|
|
||||||
};
|
|
||||||
|
|
||||||
let validatedPlaylists = {};
|
|
||||||
const VALIDATION_TTL = 3600000; // 1 hour
|
|
||||||
|
|
||||||
const validatePlaylist = async (playlistId) => {
|
|
||||||
try {
|
|
||||||
const response = await youtube.playlists.list({
|
|
||||||
key: API_KEY,
|
|
||||||
part: 'snippet,contentDetails',
|
|
||||||
id: playlistId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.data.items || response.data.items.length === 0) {
|
|
||||||
console.log(`Playlist ${playlistId} not found or empty`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
validatedPlaylists[playlistId] = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
valid: true
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Failed to validate playlist ${playlistId}:`, error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateAndCleanPlaylists = async () => {
|
|
||||||
const now = Date.now();
|
|
||||||
const validPlaylistIds = [];
|
|
||||||
|
|
||||||
for (const [genre, playlistId] of Object.entries(PLAYLISTS)) {
|
|
||||||
if (validatedPlaylists[playlistId] &&
|
|
||||||
(now - validatedPlaylists[playlistId].timestamp) < VALIDATION_TTL) {
|
|
||||||
if (validatedPlaylists[playlistId].valid) {
|
|
||||||
validPlaylistIds.push([genre, playlistId]);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValid = await validatePlaylist(playlistId);
|
|
||||||
if (isValid) {
|
|
||||||
validPlaylistIds.push([genre, playlistId]);
|
|
||||||
} else {
|
|
||||||
console.log(`Removing invalid playlist: ${genre} (${playlistId})`);
|
|
||||||
delete PLAYLISTS[genre];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return validPlaylistIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
const API_KEY = process.env.YOUTUBE_API_KEY;
|
|
||||||
|
|
||||||
const cachedSongsByPlaylist = {};
|
|
||||||
|
|
||||||
const CACHE_TTL = 3600000; // 1 hour
|
const CACHE_TTL = 3600000; // 1 hour
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches songs from YouTube playlist and returns them
|
* Fetches songs from YouTube playlist and returns them
|
||||||
*/
|
*/
|
||||||
const fetchPlaylistSongs = async (playlistId = null) => {
|
const fetchPlaylistSongs = async () => {
|
||||||
if (!playlistId) {
|
|
||||||
console.warn("No playlist ID provided, using default");
|
|
||||||
playlistId = PLAYLISTS.eighties;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (cachedSongsByPlaylist[playlistId] &&
|
if (cachedSongs && cachedSongs.length > 0 && (now - lastFetchTime) < CACHE_TTL) {
|
||||||
cachedSongsByPlaylist[playlistId].songs.length > 0 &&
|
console.log("Returning cached YouTube songs:", cachedSongs.length);
|
||||||
(now - cachedSongsByPlaylist[playlistId].timestamp) < CACHE_TTL) {
|
return cachedSongs;
|
||||||
console.log(`Using cached songs for playlist ${playlistId}`);
|
|
||||||
return cachedSongsByPlaylist[playlistId].songs;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(`Fetching fresh songs from YouTube API for playlist ${playlistId}...`);
|
console.log("Fetching songs from YouTube API...");
|
||||||
const playlistUrl = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&maxResults=50&playlistId=${playlistId}&key=${YOUTUBE_API_KEY}`;
|
const playlistUrl = `https://www.googleapis.com/youtube/v3/playlistItems?part=snippet,contentDetails&maxResults=50&playlistId=${PLAYLIST_ID}&key=${YOUTUBE_API_KEY}`;
|
||||||
const response = await fetch(playlistUrl);
|
const response = await fetch(playlistUrl);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
@ -98,6 +27,7 @@ const fetchPlaylistSongs = async (playlistId = null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!data.items || !data.items.length) {
|
if (!data.items || !data.items.length) {
|
||||||
|
console.error("No items found in YouTube playlist");
|
||||||
throw new Error("No songs found in the playlist");
|
throw new Error("No songs found in the playlist");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,122 +36,36 @@ const fetchPlaylistSongs = async (playlistId = null) => {
|
|||||||
youtubeId: item.contentDetails.videoId,
|
youtubeId: item.contentDetails.videoId,
|
||||||
title: item.snippet.title,
|
title: item.snippet.title,
|
||||||
artist: item.snippet.videoOwnerChannelTitle || "Unknown Artist",
|
artist: item.snippet.videoOwnerChannelTitle || "Unknown Artist",
|
||||||
coverUrl: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.default?.url,
|
coverUrl: item.snippet.thumbnails.high?.url || item.snippet.thumbnails.default?.url ||
|
||||||
playlistId: playlistId
|
"https://place-hold.it/500x500/333/fff?text=No%20Thumbnail",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
cachedSongsByPlaylist[playlistId] = {
|
cachedSongs = songs;
|
||||||
songs,
|
lastFetchTime = now;
|
||||||
timestamp: now
|
console.log("Fetched and cached YouTube songs:", songs.length);
|
||||||
};
|
|
||||||
|
|
||||||
return songs;
|
return songs;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching YouTube playlist ${playlistId}:`, error);
|
console.error("Error fetching YouTube playlist:", error);
|
||||||
return cachedSongsByPlaylist[playlistId]?.songs || getDefaultSongs();
|
|
||||||
|
if (cachedSongs && cachedSongs.length > 0) {
|
||||||
|
console.log("Using last cached songs due to API error");
|
||||||
|
return cachedSongs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
id: 1,
|
||||||
|
title: "Could not load songs",
|
||||||
|
artist: "API Error",
|
||||||
|
coverUrl: "https://place-hold.it/500x500/f00/fff?text=Error",
|
||||||
|
youtubeId: "dQw4w9WgXcQ"
|
||||||
|
}];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAvailableSongIds = async (playlistId) => {
|
const getAvailableSongIds = async () => {
|
||||||
const songs = await fetchPlaylistSongs(playlistId);
|
const songs = await fetchPlaylistSongs();
|
||||||
return songs.map(song => song.id);
|
return songs.map(song => song.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getPlaylistDetails(availablePlaylists = null) {
|
module.exports = {fetchPlaylistSongs, getAvailableSongIds};
|
||||||
try {
|
|
||||||
const playlistsToUse = availablePlaylists || await getRandomPlaylists(3);
|
|
||||||
const details = {};
|
|
||||||
const validationPromises = [];
|
|
||||||
|
|
||||||
for (const [genre, playlistId] of Object.entries(playlistsToUse)) {
|
|
||||||
validationPromises.push(
|
|
||||||
youtube.playlists.list({
|
|
||||||
key: API_KEY,
|
|
||||||
part: 'snippet,contentDetails',
|
|
||||||
id: playlistId
|
|
||||||
}).then(response => {
|
|
||||||
if (response.data.items?.[0]) {
|
|
||||||
const playlist = response.data.items[0];
|
|
||||||
details[genre] = {
|
|
||||||
id: playlistId,
|
|
||||||
title: playlist.snippet.title,
|
|
||||||
description: playlist.snippet.description,
|
|
||||||
thumbnail: playlist.snippet.thumbnails.maxres || playlist.snippet.thumbnails.high,
|
|
||||||
songCount: playlist.contentDetails.itemCount,
|
|
||||||
votes: 0
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.warn(`Playlist not found: ${genre} (${playlistId})`);
|
|
||||||
}
|
|
||||||
}).catch(error => {
|
|
||||||
console.error(`Error fetching playlist ${playlistId}:`, error);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Promise.all(validationPromises);
|
|
||||||
|
|
||||||
if (Object.keys(details).length === 0) {
|
|
||||||
const response = await youtube.playlists.list({
|
|
||||||
key: API_KEY,
|
|
||||||
part: 'snippet,contentDetails',
|
|
||||||
id: PLAYLISTS.seventies
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.items?.[0]) {
|
|
||||||
const playlist = response.data.items[0];
|
|
||||||
details.seventies = {
|
|
||||||
id: PLAYLISTS.seventies,
|
|
||||||
title: playlist.snippet.title,
|
|
||||||
description: playlist.snippet.description,
|
|
||||||
thumbnail: playlist.snippet.thumbnails.maxres || playlist.snippet.thumbnails.high,
|
|
||||||
songCount: playlist.contentDetails.itemCount,
|
|
||||||
votes: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(details).length === 0) {
|
|
||||||
throw new Error("No valid playlists found");
|
|
||||||
}
|
|
||||||
|
|
||||||
return details;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching playlist details:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRandomPlaylists = async (count = 3) => {
|
|
||||||
try {
|
|
||||||
const allPlaylists = Object.entries(PLAYLISTS);
|
|
||||||
if (allPlaylists.length === 0) {
|
|
||||||
throw new Error("No playlists configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
const shuffled = [...allPlaylists].sort(() => Math.random() - 0.5);
|
|
||||||
const selected = shuffled.slice(0, Math.min(count, allPlaylists.length));
|
|
||||||
|
|
||||||
const result = {};
|
|
||||||
for (const [genre, playlistId] of selected) {
|
|
||||||
result[genre] = playlistId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error getting random playlists:", error);
|
|
||||||
return {
|
|
||||||
seventies: PLAYLISTS.seventies
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
fetchPlaylistSongs,
|
|
||||||
getAvailableSongIds,
|
|
||||||
PLAYLISTS,
|
|
||||||
getPlaylistDetails,
|
|
||||||
getRandomPlaylists,
|
|
||||||
validateAndCleanPlaylists,
|
|
||||||
validatePlaylist
|
|
||||||
};
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user