diff --git a/client/src/pages/WaitingRoom/WaitingRoom.jsx b/client/src/pages/WaitingRoom/WaitingRoom.jsx
index 62cbc97..6aee2f5 100644
--- a/client/src/pages/WaitingRoom/WaitingRoom.jsx
+++ b/client/src/pages/WaitingRoom/WaitingRoom.jsx
@@ -15,14 +15,17 @@ export const WaitingRoom = () => {
const [isHost, setIsHost] = useState(false);
const [username, setUsername] = useState("");
const [copied, setCopied] = useState(false);
+ const [playlists, setPlaylists] = useState({});
+ const [votes, setVotes] = useState({});
+ const [selectedPlaylist, setSelectedPlaylist] = useState(null);
const messageEndRef = useRef(null);
useEffect(() => {
- // Check if the user is a host and get other initial data
send("check-host-status");
send("get-user-info");
send("get-room-users");
send("get-room-code");
+ send("get-playlist-options");
const handleHostStatus = (status) => {
setIsHost(status.isHost);
@@ -72,6 +75,14 @@ export const WaitingRoom = () => {
setCurrentState("Game");
};
+ const handlePlaylistOptions = (options) => {
+ setPlaylists(options);
+ };
+
+ const handleVotesUpdated = (newVotes) => {
+ setVotes(newVotes);
+ };
+
// Register event listeners
const cleanupHostStatus = on("host-status", handleHostStatus);
const cleanupUserInfo = on("user-info", handleUserInfo);
@@ -82,8 +93,9 @@ export const WaitingRoom = () => {
const cleanupUserDisconnected = on("user-disconnected", handleUserDisconnected);
const cleanupChatMessage = on("chat-message", handleChatMessage);
const cleanupGameStarted = on("game-started", handleGameStarted);
+ const cleanupPlaylistOptions = on("playlist-options", handlePlaylistOptions);
+ const cleanupVotesUpdated = on("playlist-votes-updated", handleVotesUpdated);
- // Add welcome message
setMessages([{
system: true,
text: "Welcome to the waiting room! Waiting for others to join..."
@@ -99,6 +111,8 @@ export const WaitingRoom = () => {
cleanupUserDisconnected();
cleanupChatMessage();
cleanupGameStarted();
+ cleanupPlaylistOptions();
+ cleanupVotesUpdated();
};
}, [on, send, socket, setCurrentState]);
@@ -125,7 +139,6 @@ export const WaitingRoom = () => {
};
const handleLeaveRoom = () => {
- // Disconnect from the current room
if (socket) {
socket.disconnect();
}
@@ -138,7 +151,40 @@ export const WaitingRoom = () => {
setTimeout(() => setCopied(false), 2000);
};
- const minPlayersToStart = 1; // Set your minimum players requirement here
+ const handleVote = (playlistId) => {
+ setSelectedPlaylist(playlistId);
+ send("vote-playlist", { playlistId });
+ };
+
+ const getVoteCount = (playlistId) => {
+ return votes[playlistId]?.length || 0;
+ };
+
+ const renderPlaylists = () => (
+
+ {Object.entries(playlists).map(([genre, playlist]) => (
+
handleVote(playlist.id)}
+ >
+
+

+
+ {getVoteCount(playlist.id)}
+ votes
+
+
+
+
{genre.toUpperCase()}
+
{playlist.songCount} songs
+
+
+ ))}
+
+ );
+
+ const minPlayersToStart = 1;
const canStartGame = isHost && users.length >= minPlayersToStart;
return (
@@ -227,6 +273,11 @@ export const WaitingRoom = () => {
+
+
+
Vote for Playlist
+ {renderPlaylists()}
+
);
diff --git a/client/src/pages/WaitingRoom/styles.sass b/client/src/pages/WaitingRoom/styles.sass
index 3bae174..d963d2d 100644
--- a/client/src/pages/WaitingRoom/styles.sass
+++ b/client/src/pages/WaitingRoom/styles.sass
@@ -330,6 +330,86 @@
font-size: 1.4rem
margin: 0
+.playlist-section
+ margin-bottom: 30px
+
+ h2
+ color: $white
+ text-align: center
+ margin-bottom: 20px
+ font-size: 24px
+ text-shadow: 0 0 10px rgba(255, 255, 255, 0.5)
+
+.playlists-grid
+ display: grid
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr))
+ gap: 20px
+ padding: 20px
+
+.playlist-card
+ background: rgba(255, 255, 255, 0.1)
+ border-radius: 15px
+ overflow: hidden
+ transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)
+ cursor: pointer
+ border: 2px solid transparent
+
+ &:hover
+ transform: translateY(-5px)
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3)
+ background: rgba(255, 255, 255, 0.15)
+
+ &.selected
+ border-color: $yellow
+ box-shadow: 0 0 30px rgba($yellow, 0.3)
+
+ .playlist-thumbnail
+ position: relative
+ width: 100%
+ padding-top: 56.25%
+
+ img
+ position: absolute
+ top: 0
+ left: 0
+ width: 100%
+ height: 100%
+ object-fit: cover
+
+ .vote-count
+ position: absolute
+ top: 10px
+ right: 10px
+ background: rgba(0, 0, 0, 0.8)
+ padding: 8px 12px
+ border-radius: 20px
+ display: flex
+ flex-direction: column
+ align-items: center
+
+ span
+ color: $white
+
+ &:first-child
+ font-size: 24px
+ font-weight: bold
+
+ &.vote-label
+ font-size: 12px
+ opacity: 0.8
+
+ .playlist-info
+ padding: 15px
+
+ h3
+ color: $white
+ margin-bottom: 5px
+ font-size: 18px
+
+ p
+ color: rgba(255, 255, 255, 0.7)
+ font-size: 14px
+
@keyframes pop
0%
transform: scale(1)
diff --git a/package.json b/package.json
index 7016956..04bb4be 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
},
"dependencies": {
"express": "^4.21.2",
+ "googleapis": "^146.0.0",
"socket.io": "^4.8.1"
},
"devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0cd3c7d..6d7fec9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -11,6 +11,9 @@ importers:
express:
specifier: ^4.21.2
version: 4.21.2
+ googleapis:
+ specifier: ^146.0.0
+ version: 146.0.0
socket.io:
specifier: ^4.8.1
version: 4.8.1
@@ -37,6 +40,10 @@ packages:
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
engines: {node: '>= 0.6'}
+ agent-base@7.1.3:
+ resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
+ engines: {node: '>= 14'}
+
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
@@ -55,10 +62,16 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ base64-js@1.5.1:
+ resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+
base64id@2.0.0:
resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==}
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:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@@ -74,6 +87,9 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
+ buffer-equal-constant-time@1.0.1:
+ resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
+
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
@@ -165,6 +181,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
+ ecdsa-sig-formatter@1.0.11:
+ resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
+
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
@@ -214,6 +233,9 @@ packages:
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
engines: {node: '>= 0.10.0'}
+ extend@3.0.2:
+ resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
+
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
@@ -238,6 +260,14 @@ packages:
function-bind@1.1.2:
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:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
@@ -254,10 +284,30 @@ packages:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
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:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
+ gtoken@7.1.0:
+ resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==}
+ engines: {node: '>=14.0.0'}
+
has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
@@ -278,6 +328,10 @@ packages:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'}
+ https-proxy-agent@7.0.6:
+ resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
+ engines: {node: '>= 14'}
+
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
@@ -312,6 +366,19 @@ packages:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
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:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@@ -356,6 +423,15 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
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:
resolution: {integrity: sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==}
engines: {node: '>=10'}
@@ -511,6 +587,9 @@ packages:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true
+ tr46@0.0.3:
+ resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
+
tree-kill@1.2.2:
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
hasBin: true
@@ -532,14 +611,27 @@ packages:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
+ url-template@2.0.8:
+ resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
+
utils-merge@1.0.1:
resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==}
engines: {node: '>= 0.4.0'}
+ uuid@9.0.1:
+ resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
+ hasBin: true
+
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
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:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -585,6 +677,8 @@ snapshots:
mime-types: 2.1.35
negotiator: 0.6.3
+ agent-base@7.1.3: {}
+
ansi-regex@5.0.1: {}
ansi-styles@4.3.0:
@@ -600,8 +694,12 @@ snapshots:
balanced-match@1.0.2: {}
+ base64-js@1.5.1: {}
+
base64id@2.0.0: {}
+ bignumber.js@9.1.2: {}
+
binary-extensions@2.3.0: {}
body-parser@1.20.3:
@@ -630,6 +728,8 @@ snapshots:
dependencies:
fill-range: 7.1.1
+ buffer-equal-constant-time@1.0.1: {}
+
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
@@ -720,6 +820,10 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
+ ecdsa-sig-formatter@1.0.11:
+ dependencies:
+ safe-buffer: 5.2.1
+
ee-first@1.1.1: {}
emoji-regex@8.0.0: {}
@@ -796,6 +900,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ extend@3.0.2: {}
+
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
@@ -821,6 +927,26 @@ snapshots:
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-intrinsic@1.3.0:
@@ -845,8 +971,50 @@ snapshots:
dependencies:
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: {}
+ gtoken@7.1.0:
+ dependencies:
+ gaxios: 6.7.1
+ jws: 4.0.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
has-flag@3.0.0: {}
has-flag@4.0.0: {}
@@ -865,6 +1033,13 @@ snapshots:
statuses: 2.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:
dependencies:
safer-buffer: 2.1.2
@@ -889,6 +1064,23 @@ snapshots:
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: {}
math-intrinsics@1.1.0: {}
@@ -917,6 +1109,10 @@ snapshots:
negotiator@0.6.3: {}
+ node-fetch@2.7.0:
+ dependencies:
+ whatwg-url: 5.0.0
+
nodemon@3.1.9:
dependencies:
chokidar: 3.6.0
@@ -1107,6 +1303,8 @@ snapshots:
touch@3.1.1: {}
+ tr46@0.0.3: {}
+
tree-kill@1.2.2: {}
tslib@2.8.1: {}
@@ -1122,10 +1320,21 @@ snapshots:
unpipe@1.0.0: {}
+ url-template@2.0.8: {}
+
utils-merge@1.0.1: {}
+ uuid@9.0.1: {}
+
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:
dependencies:
ansi-styles: 4.3.0
diff --git a/server/controller/room.js b/server/controller/room.js
index 2c46cee..ce7876c 100644
--- a/server/controller/room.js
+++ b/server/controller/room.js
@@ -10,16 +10,22 @@ module.exports.roomExists = (roomId) => rooms[roomId] !== undefined;
module.exports.isRoomOpen = (roomId) => rooms[roomId] && rooms[roomId].state === 'waiting';
+const initializeRoom = (roomId, user) => {
+ rooms[roomId] = {
+ members: [{...user, creator: true}],
+ settings: {},
+ state: 'waiting',
+ playlistVotes: {},
+ selectedPlaylist: null
+ };
+};
+
module.exports.connectUserToRoom = (roomId, user) => {
roomId = roomId.toUpperCase();
if (rooms[roomId]) {
rooms[roomId].members.push({...user, creator: false});
} else {
- rooms[roomId] = {
- members: [{...user, creator: true}],
- settings: {},
- state: 'waiting'
- };
+ initializeRoom(roomId, user);
}
console.log(`User ${user.name} connected to room ${roomId}`);
}
@@ -132,4 +138,32 @@ module.exports.getUserName = (userId) => {
}
}
return null;
+};
+
+module.exports.voteForPlaylist = (roomId, userId, playlistId) => {
+ if (!rooms[roomId]) return false;
+
+ const room = rooms[roomId];
+ if (room.state !== 'waiting') return false;
+
+ // Remove previous vote if exists
+ const previousVote = Object.entries(room.playlistVotes)
+ .find(([_, voters]) => voters.includes(userId));
+
+ if (previousVote) {
+ room.playlistVotes[previousVote[0]] =
+ room.playlistVotes[previousVote[0]].filter(id => id !== userId);
+ }
+
+ // Add new vote
+ if (!room.playlistVotes[playlistId]) {
+ room.playlistVotes[playlistId] = [];
+ }
+ room.playlistVotes[playlistId].push(userId);
+
+ return true;
+};
+
+module.exports.getPlaylistVotes = (roomId) => {
+ return rooms[roomId]?.playlistVotes || {};
};
\ No newline at end of file
diff --git a/server/handler/connection.js b/server/handler/connection.js
index 824c202..2d47f1e 100644
--- a/server/handler/connection.js
+++ b/server/handler/connection.js
@@ -308,4 +308,24 @@ module.exports = (io) => (socket) => {
socket.emit("playlist-songs", { songs: youtubeService.getDefaultSongs() });
}
});
+
+ socket.on("get-playlist-options", async () => {
+ try {
+ const playlists = await youtubeService.getPlaylistDetails();
+ socket.emit("playlist-options", playlists);
+ } catch (error) {
+ console.error("Error fetching playlist options:", error);
+ socket.emit("error", { message: "Failed to load playlists" });
+ }
+ });
+
+ 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);
+ }
+ });
};
\ No newline at end of file
diff --git a/server/services/youtubeService.js b/server/services/youtubeService.js
index 11887c6..8d8770d 100644
--- a/server/services/youtubeService.js
+++ b/server/services/youtubeService.js
@@ -1,6 +1,17 @@
+const { google } = require('googleapis');
+const youtube = google.youtube('v3');
+
const YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;
const PLAYLIST_ID = "PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf";
+const PLAYLISTS = {
+ seventies: 'PLmXxqSJJq-yXrCPGIT2gn8b34JjOrl4Xf',
+ eighties: 'PLmXxqSJJq-yUvMWKuZQAB_8yxnjZaOZUp',
+ nineties: 'PLmXxqSJJq-yUF3jbzjF_pa--kuBuMlyQQ'
+};
+
+const API_KEY = process.env.YOUTUBE_API_KEY;
+
let cachedSongs = null;
let lastFetchTime = 0;
const CACHE_TTL = 3600000; // 1 hour
@@ -68,4 +79,38 @@ const getAvailableSongIds = async () => {
return songs.map(song => song.id);
};
-module.exports = {fetchPlaylistSongs, getAvailableSongIds};
+async function getPlaylistDetails() {
+ try {
+ const details = {};
+
+ for (const [genre, playlistId] of Object.entries(PLAYLISTS)) {
+ const response = await youtube.playlists.list({
+ key: API_KEY,
+ part: 'snippet,contentDetails',
+ id: playlistId
+ });
+
+ 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
+ };
+ }
+
+ return details;
+ } catch (error) {
+ console.error('Error fetching playlist details:', error);
+ throw error;
+ }
+}
+
+module.exports = {
+ fetchPlaylistSongs,
+ getAvailableSongIds,
+ PLAYLISTS,
+ getPlaylistDetails
+};