-
-
-
-
+ {/* Mobile Dropdown */}
+
+ {navItems.map((item) => (
+
+ ))}

navigate("/profile")}
+ onClick={() => {
+ navigate("/profile");
+ setMenuOpen(false);
+ }}
alt="profile"
- className="w-10 h-10 rounded-full border-[2px] border-[#FFFB00] shadow-[0_0_10px_#FFFB00] cursor-pointer"
+ className="w-12 h-12 rounded-full border-[2px] border-[#FFFB00] shadow-[0_0_10px_#FFFB00] cursor-pointer relative z-10"
/>
diff --git a/client/src/config/firebase.js b/client/src/config/firebase.js
index e69de29..3f4724b 100644
--- a/client/src/config/firebase.js
+++ b/client/src/config/firebase.js
@@ -0,0 +1,17 @@
+import { initializeApp } from 'firebase/app';
+import { getAuth, GoogleAuthProvider } from 'firebase/auth';
+
+const firebaseConfig = {
+ apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
+ authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
+ projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
+ storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
+ appId: import.meta.env.VITE_FIREBASE_APP_ID,
+};
+
+const app = initializeApp(firebaseConfig);
+const auth = getAuth(app);
+const provider = new GoogleAuthProvider();
+
+export { auth, provider };
diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx
index e69de29..97526ce 100644
--- a/client/src/context/AuthContext.jsx
+++ b/client/src/context/AuthContext.jsx
@@ -0,0 +1,72 @@
+import React, { createContext, useState, useEffect } from 'react';
+
+export const AuthContext = createContext();
+
+const AuthProvider = ({ children }) => {
+ const [user, setUser] = useState(null); // Firebase user
+ const [isGuest, setIsGuest] = useState(false);
+ const [guestName, setGuestName] = useState('');
+ const [guestAvatar, setGuestAvatar] = useState('');
+ const [authType, setAuthType] = useState(''); // 'user' | 'guest' | ''
+
+ useEffect(() => {
+ // Load Firebase user if exists
+ const storedUser = JSON.parse(localStorage.getItem('user'));
+ if (storedUser) {
+ setUser(storedUser);
+ setAuthType('user');
+ }
+
+ // Load guest data if exists
+ const storedGuest = JSON.parse(localStorage.getItem('guest'));
+ if (storedGuest && storedGuest.name) {
+ setIsGuest(true);
+ setGuestName(storedGuest.name);
+ setGuestAvatar(storedGuest.avatar || '');
+ setAuthType('guest');
+ }
+ }, []);
+
+ const loginAsGuest = (name, avatar) => {
+ setIsGuest(true);
+ setGuestName(name);
+ setGuestAvatar(avatar);
+ setAuthType('guest');
+ localStorage.setItem('guest', JSON.stringify({ name, avatar }));
+ };
+
+ const logoutGuest = () => {
+ setIsGuest(false);
+ setGuestName('');
+ setGuestAvatar('');
+ setAuthType('');
+ localStorage.removeItem('guest');
+ };
+
+ const logout = () => {
+ setUser(null);
+ setAuthType('');
+ localStorage.removeItem('user');
+ logoutGuest(); // Also clears guest if any
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default AuthProvider;
diff --git a/client/src/data/avatarList.js b/client/src/data/avatarList.js
index e69de29..9fd12e9 100644
--- a/client/src/data/avatarList.js
+++ b/client/src/data/avatarList.js
@@ -0,0 +1,17 @@
+const avatarList = [
+ "/avatars/1.png",
+ "/avatars/2.png",
+ "/avatars/3.png",
+ "/avatars/4.png",
+ "/avatars/5.png",
+ "/avatars/6.png",
+ "/avatars/7.png",
+ "/avatars/8.png",
+ "/avatars/9.png",
+ "/avatars/10.png",
+ "/avatars/11.png",
+ "/avatars/12.png"
+ ];
+
+ export default avatarList;
+
\ No newline at end of file
diff --git a/client/src/index.css b/client/src/index.css
index d2087b3..b61311b 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -4,4 +4,30 @@
@theme {
--font-silkscreen: "Silkscreen", sans-serif;
- }
\ No newline at end of file
+ }
+
+/* Custom Scrollbar Styles */
+.custom-scrollbar {
+ /* Webkit browsers */
+ scrollbar-width: thin;
+ scrollbar-color: #FBBF24 #374151;
+}
+
+.custom-scrollbar::-webkit-scrollbar {
+ width: 8px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-track {
+ background: #374151;
+ border-radius: 10px;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb {
+ background: #FBBF24;
+ border-radius: 10px;
+ border: 1px solid #374151;
+}
+
+.custom-scrollbar::-webkit-scrollbar-thumb:hover {
+ background: #F59E0B;
+}
\ No newline at end of file
diff --git a/client/src/pages/AboutDev.jsx b/client/src/pages/AboutDev.jsx
index e69de29..3d4b989 100644
--- a/client/src/pages/AboutDev.jsx
+++ b/client/src/pages/AboutDev.jsx
@@ -0,0 +1,185 @@
+import { useEffect, useState } from "react";
+import Navbar from "../components/common/Navbar";
+
+const AboutDev = () => {
+ const [displayText, setDisplayText] = useState("");
+ const fullText = "Hey, I'm Yogesh!👋";
+
+ useEffect(() => {
+ let i = 0;
+ const interval = setInterval(() => {
+ if (i < fullText.length) {
+ setDisplayText(fullText.slice(0, i + 1));
+ i++;
+ } else {
+ clearInterval(interval);
+ }
+ }, 100);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ const skills = [
+ { name: "Python", icon: "fab fa-python" },
+ { name: "JavaScript", icon: "fab fa-js" },
+ { name: "HTML5", icon: "fab fa-html5" },
+ { name: "CSS3", icon: "fab fa-css3-alt" },
+ { name: "React", icon: "fab fa-react" },
+ { name: "Node.js", icon: "fab fa-node-js" },
+ { name: "Express.js", icon: "fas fa-server" },
+ { name: "Git", icon: "fab fa-git-alt" },
+ { name: "MongoDB", icon: "fas fa-database" },
+ { name: "GitHub", icon: "fab fa-github" },
+ { name: "Firebase", icon: "fas fa-fire" },
+ { name: "Socket.io", icon: "fas fa-plug" },
+ { name: "Tailwind CSS", icon: "fab fa-css3" },
+ ];
+
+ return (
+
+
+
+ {/* Fixed Navbar */}
+
+
+ {/* Main Content */}
+
+ {/* Profile Image */}
+
+

+
+
+ {/* Info Card */}
+
+
+
+ {displayText}
+
+
+
+ Developer of{" "}
+ Guessync
+
+
+
+ I'm a full-stack developer who loves crafting playful, real-time web apps.
+ Guessync was built pixel-by-pixel to bring that retro arcade magic back to life,
+ blending music, fast guessing, and teamwork into pure fun. 🚀
+
+
+
+
My Tech Stack
+
+ {skills.map((skill, index) => (
+
+ {skill.name}
+
+
+ ))}
+
+
+
+
+ {/* Social + CTA */}
+
+
+
+
+ );
+};
+
+export default AboutDev;
\ No newline at end of file
diff --git a/client/src/pages/CreateRoom.jsx b/client/src/pages/CreateRoom.jsx
index e69de29..fb63508 100644
--- a/client/src/pages/CreateRoom.jsx
+++ b/client/src/pages/CreateRoom.jsx
@@ -0,0 +1,429 @@
+import React, { useEffect, useState } from "react";
+import Navbar from "../components/common/Navbar";
+import { useNavigate } from "react-router-dom";
+import socket from "../socket";
+import sendIcon from "../assets/send.png";
+import spotcon from "../assets/spotify.png";
+
+
+const DEFAULT_PLAYLISTS = {
+ Tamil: "316WvxScpeCbfvWVrTHfPa",
+ Hindi: "4stlIpoPS7uKCsmUA7D8KZ",
+ English: "2clGinIH6s1bj2TclAgKzW",
+ Malayalam: "5tU4P1GOBDBs9nwfok79yD",
+ Telugu: "1llHjtjECBo12ChwOGe38L",
+ Kannada: "6utix5lfPoZkBirWlRujqa"
+};
+
+const CreateRoom = () => {
+ const [roomCode, setRoomCode] = useState("");
+ const [players, setPlayers] = useState(1);
+ const [rounds, setRounds] = useState(1);
+ const [duration, setDuration] = useState(30);
+ const [spotifyInputVisible, setSpotifyInputVisible] = useState(false);
+ const [spotifyValue, setSpotifyValue] = useState("");
+ const [spotifyConfirmed, setSpotifyConfirmed] = useState(false);
+ const [showNotification, setShowNotification] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const [selectedLanguage, setSelectedLanguage] = useState("");
+ const [rulesFile, setRulesFile] = useState(null);
+ const [showRulesModal, setShowRulesModal] = useState(false);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const handleResize = () => {
+ if (window.innerWidth >= 1024) {
+ document.body.style.overflow = "hidden"; // lock scroll
+ } else {
+ document.body.style.overflow = "auto"; // allow scroll
+ }
+ };
+
+ handleResize(); // run on mount
+ window.addEventListener("resize", handleResize);
+
+ return () => {
+ document.body.style.overflow = "auto"; // cleanup
+ window.removeEventListener("resize", handleResize);
+ };
+}, []);
+
+
+ useEffect(() => {
+ window.scrollTo(0, 0);
+ const code = Math.floor(100000 + Math.random() * 900000);
+ setRoomCode(code);
+ }, []);
+
+ const toggleLanguage = (lang) => {
+ setSelectedLanguage((prev) => (prev === lang ? "" : lang));
+ };
+
+ const handleCreateRoom = async () => {
+ const token = localStorage.getItem("token");
+ const user = JSON.parse(localStorage.getItem("user"));
+ if (!token || !user) {
+ alert("Please log in first!");
+ return;
+ }
+
+ socket.auth = { uid: user.uid };
+ socket.connect();
+
+ let playlistId = null;
+ let useSpotify = false;
+ let rulesText = "";
+
+ if (selectedLanguage && DEFAULT_PLAYLISTS[selectedLanguage]) {
+ playlistId = DEFAULT_PLAYLISTS[selectedLanguage];
+ useSpotify = true;
+ } else if (spotifyConfirmed && spotifyValue.trim()) {
+ playlistId = spotifyValue.split("playlist/")[1]?.split("?")[0];
+ if (!playlistId) {
+ alert("❌ Invalid Spotify Playlist URL");
+ return;
+ }
+ useSpotify = true;
+ }
+
+ if (rulesFile) {
+ const reader = new FileReader();
+ reader.onload = async (e) => {
+ rulesText = e.target.result;
+ await sendRequest();
+ };
+ reader.readAsText(rulesFile);
+ } else {
+ await sendRequest();
+ }
+
+ async function sendRequest() {
+ const payload = {
+ uid: user.uid,
+ name: user.name,
+ avatar: user.avatar || "https://i.imgur.com/placeholder.png",
+ useSpotify,
+ playlistId,
+ players,
+ rounds,
+ duration,
+ language: selectedLanguage,
+ code: roomCode,
+ rules: rulesText,
+ };
+
+ try {
+ const res = await fetch("https://guessync.onrender.com/api/room/create", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify(payload),
+ });
+ const data = await res.json();
+
+ if (res.ok) {
+ const room = {
+ code: data.code,
+ hostUID: user.uid,
+ players: [{ uid: user.uid, name: user.name, avatar: user.avatar }],
+ };
+ localStorage.setItem("room", JSON.stringify(room));
+ socket.emit("join-room", { roomCode: data.code, user });
+ navigate("/waiting-room");
+ } else {
+ alert("❌ Failed to create room: " + data.message);
+ }
+ } catch (err) {
+ console.error("Error creating room:", err);
+ alert("❌ Something went wrong.");
+ }
+ }
+ };
+
+ const showCreateBtn = selectedLanguage || spotifyConfirmed;
+
+return (
+
+
+
+
+ {/* Title */}
+
+ Create A New Room
+
+
+ {/* Room Code Section */}
+
+
+ ROOM CODE:
+
+
+
+ {roomCode}
+
+

{
+ navigator.clipboard.writeText(roomCode);
+ setShowNotification(true);
+ setTimeout(() => setShowNotification(false), 2000);
+ }}
+ />
+ {showNotification && (
+
+ Code copied!
+
+ )}
+
+
+
+ {/* Custom Controls */}
+
+ {/* Only show Upload Rules button on large screens */}
+
+
+ CUSTOM CONTROLS
+
+
+
+
+ {/* Show only the heading on smaller screens */}
+
+
+ CUSTOM CONTROLS
+
+
+
+ {/* Players and Rounds Controls */}
+ {[
+ { label: "NO. OF PLAYERS", value: players, set: setPlayers, max: 12 },
+ { label: "NO. OF ROUNDS", value: rounds, set: setRounds, max: 20 },
+ ].map((ctrl, i) => (
+
+
+
+
+
+ {ctrl.value}
+
+
+
+
+ ))}
+
+ {/* Duration Control */}
+
+
+
+ {[30, 60].map((val) => (
+
+ ))}
+
+
+
+
+ {/* Action Buttons - Centered and enlarged for larger devices */}
+
+ {!spotifyConfirmed && !showCreateBtn && (
+
+ )}
+
+ {!selectedLanguage && !showCreateBtn && (
+
+ {!spotifyInputVisible ? (
+
+ ) : (
+
+
setSpotifyValue(e.target.value)}
+ />
+

{
+ if (spotifyValue.trim()) setSpotifyConfirmed(true);
+ }}
+ />
+
+ )}
+
+ )}
+
+ {selectedLanguage && (
+
+ Using {selectedLanguage} playlist
+
+ )}
+
+ {showCreateBtn && (
+
+ )}
+
+
+ {/* Language Selection Modal */}
+ {showModal && (
+
+
+
+
+ Pick Language
+
+
+ Select one language
+
+
+ {Object.keys(DEFAULT_PLAYLISTS).map((lang) => (
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Rules Upload Modal */}
+ {showRulesModal && (
+
+
+
+
+ Optional
+
+
+ Upload Rules
+
+
+ UPLOAD A .TXT FILE WITH GAME
+ RULES LIKE:
+
– NO SHAZAM
– NO CHEATING
– NO SPAMMING
–
+ BE RESPECTFUL
+
+
+ {rulesFile && (
+
+
+ {rulesFile.name}
+
+
+
+ )}
+
+
+
+ )}
+
+
+ );
+};
+
+export default CreateRoom;
\ No newline at end of file
diff --git a/client/src/pages/GameRoom.jsx b/client/src/pages/GameRoom.jsx
index e69de29..7a26e09 100644
--- a/client/src/pages/GameRoom.jsx
+++ b/client/src/pages/GameRoom.jsx
@@ -0,0 +1,728 @@
+import React, { useEffect, useState, useRef } from "react";
+import socket from "../socket";
+import ColorThief from "colorthief";
+import Navbar from "../components/common/Navbar";
+
+
+const GameRoom = () => {
+ const [song, setSong] = useState(null);
+ const [guess, setGuess] = useState("");
+ const [chat, setChat] = useState([]);
+ const [round, setRound] = useState(1);
+ const [timer, setTimer] = useState(0);
+ const [gameOver, setGameOver] = useState(false);
+ const [leaderboard, setLeaderboard] = useState([]);
+ const [hintRevealed, setHintRevealed] = useState({
+ movie: false,
+ composer: false,
+ cover: false,
+ aiHint: "",
+ });
+ const [players, setPlayers] = useState([]);
+ const [voteCounts, setVoteCounts] = useState({
+ movie: 0,
+ composer: 0,
+ ai: 0,
+ });
+ const [loadingNext, setLoadingNext] = useState(false);
+ const [correctGuess, setCorrectGuess] = useState(false);
+ const [roundEnded, setRoundEnded] = useState(false);
+ const [countdown, setCountdown] = useState(5);
+ const [hintsUsed, setHintsUsed] = useState({
+ movie: false,
+ composer: false,
+ ai: false,
+ });
+ const [currentSongDetails, setCurrentSongDetails] = useState(null);
+ const [showPlayButton, setShowPlayButton] = useState(false);
+ const [isFirstRound, setIsFirstRound] = useState(true);
+
+ const audioRef = useRef(null);
+ const blobCanvasRef = useRef(null);
+ const blobsRef = useRef([]);
+ const animationRef = useRef(null);
+ const targetColorsRef = useRef([
+ [255, 251, 0],
+ [78, 78, 78],
+ [40, 40, 40],
+ [255, 200, 0],
+ ]);
+ const currentColorsRef = useRef([
+ [255, 251, 0],
+ [78, 78, 78],
+ [40, 40, 40],
+ [255, 200, 0],
+ ]);
+ const chatEndRef = useRef(null);
+ const gameStartedRef = useRef(false);
+
+ const user = JSON.parse(localStorage.getItem("user"));
+ const room = JSON.parse(localStorage.getItem("room"));
+ const roomCode = room?.code;
+
+ class Blob {
+ constructor(x, y, radius, colorIndex, speed, drift) {
+ this.x = x;
+ this.y = y;
+ this.radius = radius;
+ this.colorIndex = colorIndex;
+ this.speed = speed;
+ this.angle = Math.random() * Math.PI * 2;
+ this.drift = drift;
+ this.alpha = 0.7 + Math.random() * 0.3;
+ }
+
+ update(canvas) {
+ this.angle += this.drift;
+ this.x += Math.cos(this.angle) * this.speed;
+ this.y += Math.sin(this.angle) * this.speed;
+ if (this.x < -this.radius) this.x = canvas.width + this.radius;
+ if (this.x > canvas.width + this.radius) this.x = -this.radius;
+ if (this.y < -this.radius) this.y = canvas.height + this.radius;
+ if (this.y > canvas.height + this.radius) this.y = -this.radius;
+ }
+
+ draw(ctx) {
+ const [r, g, b] =
+ currentColorsRef.current[
+ this.colorIndex % currentColorsRef.current.length
+ ];
+ ctx.beginPath();
+ ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${this.alpha})`;
+ ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+ }
+
+ const initBlobCanvas = () => {
+ const canvas = blobCanvasRef.current;
+ if (!canvas) return () => {};
+
+ const ctx = canvas.getContext("2d");
+
+ const resizeCanvas = () => {
+ if (!canvas || !canvas.parentElement) return;
+ canvas.width = canvas.parentElement.offsetWidth;
+ canvas.height = canvas.parentElement.offsetHeight;
+ };
+ resizeCanvas();
+
+ const generateBlobs = () => {
+ if (blobsRef.current.length > 0) return;
+
+ const blobCount = 65 + Math.floor(Math.random() * 16);
+
+ for (let i = 0; i < blobCount; i++) {
+ blobsRef.current.push(
+ new Blob(
+ Math.random() * canvas.width,
+ Math.random() * canvas.height,
+ 20 + Math.random() * 100,
+ i % targetColorsRef.current.length,
+ 0.3 + Math.random() * 0.7,
+ (Math.random() - 0.5) * 0.02
+ )
+ );
+ }
+ };
+
+ const updateColors = () => {
+ for (let i = 0; i < currentColorsRef.current.length; i++) {
+ for (let j = 0; j < 3; j++) {
+ currentColorsRef.current[i][j] +=
+ (targetColorsRef.current[i][j] - currentColorsRef.current[i][j]) *
+ 0.1;
+ if (
+ Math.abs(
+ currentColorsRef.current[i][j] - targetColorsRef.current[i][j]
+ ) < 1
+ ) {
+ currentColorsRef.current[i][j] = targetColorsRef.current[i][j];
+ }
+ }
+ }
+ };
+
+ const animate = () => {
+ if (!canvas) return;
+
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ updateColors();
+ blobsRef.current.forEach((blob) => {
+ blob.update(canvas);
+ blob.draw(ctx);
+ });
+ animationRef.current = requestAnimationFrame(animate);
+ };
+
+ const handleResize = () => {
+ resizeCanvas();
+ blobsRef.current.forEach((blob) => {
+ blob.x = Math.max(
+ blob.radius,
+ Math.min(canvas.width - blob.radius, blob.x)
+ );
+ blob.y = Math.max(
+ blob.radius,
+ Math.min(canvas.height - blob.radius, blob.y)
+ );
+ });
+ };
+
+ generateBlobs();
+ animate();
+ window.addEventListener("resize", handleResize);
+
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current);
+ }
+ };
+ };
+
+ const extractAlbumColors = (imageUrl) => {
+ const img = new Image();
+ img.crossOrigin = "Anonymous";
+ img.src = imageUrl;
+
+ img.onload = () => {
+ try {
+ const colorThief = new ColorThief();
+ const palette = colorThief.getPalette(img, 4);
+ targetColorsRef.current = palette;
+ } catch (err) {
+ console.error("Color extraction failed:", err);
+ }
+ };
+
+ img.onerror = () => {
+ console.error("Failed to load album image");
+ };
+ };
+
+ const scrollChatToBottom = () => {
+ chatEndRef.current?.scrollIntoView({ behavior: "smooth" });
+ };
+
+ useEffect(() => {
+ scrollChatToBottom();
+ }, [chat]);
+
+ const handlePlayFirstSong = () => {
+ if (audioRef.current && song) {
+ audioRef.current.src = `https://www.youtube.com/embed/${song.videoId}?autoplay=1&start=0&end=${timer}`;
+ setShowPlayButton(false);
+ setIsFirstRound(false);
+ }
+ };
+
+ useEffect(() => {
+ const cleanupBlobCanvas = initBlobCanvas();
+
+ socket.on("start-round", ({ song, round, duration, players }) => {
+ setSong(song);
+ setCurrentSongDetails(song);
+ setRound(round);
+ setTimer(duration);
+ setChat([]);
+ setHintRevealed({
+ movie: false,
+ composer: false,
+ cover: false,
+ aiHint: "",
+ });
+ setVoteCounts({ movie: 0, composer: 0, ai: 0 });
+ setPlayers(players || []);
+ setLoadingNext(false);
+ setCorrectGuess(false);
+ setRoundEnded(false);
+ setCountdown(3);
+
+ if (isFirstRound) {
+ setShowPlayButton(true);
+ }
+
+ if (song.cover) {
+ extractAlbumColors(song.cover);
+ }
+
+ if (!gameStartedRef.current) {
+ gameStartedRef.current = true;
+ }
+ });
+
+ socket.on("preload-next-song", ({ song, duration }) => {
+ if (audioRef.current) {
+ audioRef.current.src = `https://www.youtube.com/embed/${song.videoId}?autoplay=1&start=0&end=${duration+3}`;
+ }
+ });
+
+ socket.on("room-updated", ({ players, hintsUsed: serverHintsUsed }) => {
+ setPlayers(players);
+ if (serverHintsUsed) {
+ setHintsUsed(serverHintsUsed);
+ }
+ });
+
+ socket.on("new-guess", ({ user, text, correct }) => {
+ setChat((prev) => [...prev, { user, text, correct }]);
+ });
+
+ socket.on("correct-guess", ({ user: guesser }) => {
+ const isCurrentUser = guesser.uid === user.uid;
+ setChat((prev) => [
+ ...prev,
+ { system: true, type: "crct-guess", text: `✅ ${guesser.name} guessed it right!` },
+ ]);
+
+ if (isCurrentUser) {
+ setCorrectGuess(true);
+ setHintRevealed((prev) => ({
+ ...prev,
+ movie: true,
+ composer: true,
+ cover: true,
+ }));
+ }
+ });
+
+ socket.on("reveal-hint", ({ hintType, aiHint }) => {
+ if (hintType === "movie") {
+ setHintRevealed((prev) => ({
+ ...prev,
+ movie: true,
+ cover: true,
+ }));
+ setHintsUsed((prev) => ({ ...prev, movie: true }));
+ } else if (hintType === "composer") {
+ setHintRevealed((prev) => ({
+ ...prev,
+ composer: true,
+ }));
+ setHintsUsed((prev) => ({ ...prev, composer: true }));
+ } else if (hintType === "ai" && aiHint) {
+ setHintRevealed((prev) => ({
+ ...prev,
+ aiHint,
+ }));
+ setHintsUsed((prev) => ({ ...prev, ai: true }));
+ setChat((prev) => [
+ ...prev,
+ {
+ system: true,
+ type: "ai-hint",
+ text: (
+
+
+ IS.AI: {aiHint}
+
+ ),
+ },
+ ]);
+ }
+ });
+
+ socket.on("hint-vote-count", ({ hintType, votes }) => {
+ setVoteCounts((prev) => ({ ...prev, [hintType]: votes }));
+ });
+
+ socket.on("game-over", ({ leaderboard }) => {
+ setGameOver(true);
+ setLeaderboard(leaderboard);
+ });
+
+ socket.on("loading-next-round", () => {
+ setLoadingNext(true);
+ });
+
+ socket.on("round-ended", () => {
+ setRoundEnded(true);
+ setHintRevealed((prev) => ({
+ ...prev,
+ movie: true,
+ composer: true,
+ cover: true,
+ }));
+
+ let counter = 3;
+ setCountdown(counter);
+ const interval = setInterval(() => {
+ counter -= 1;
+ setCountdown(counter);
+ if (counter <= 0) {
+ clearInterval(interval);
+ }
+ }, 1000);
+ });
+
+ return () => {
+ socket.off("start-round");
+ socket.off("room-updated");
+ socket.off("new-guess");
+ socket.off("correct-guess");
+ socket.off("reveal-hint");
+ socket.off("hint-vote-count");
+ socket.off("game-over");
+ socket.off("loading-next-round");
+ socket.off("round-ended");
+ socket.emit("leave-room", { roomCode, user });
+ cleanupBlobCanvas();
+ };
+ }, [roomCode, user, isFirstRound]);
+
+ useEffect(() => {
+ let interval;
+ if (timer > 0 && !roundEnded) {
+ interval = setInterval(() => {
+ setTimer((prev) => {
+ if (prev <= 1) {
+ clearInterval(interval);
+ return 0;
+ }
+ return prev - 1;
+ });
+ }, 1000);
+ }
+ return () => clearInterval(interval);
+ }, [timer, roundEnded]);
+
+ const handleGuessSubmit = (e) => {
+ e.preventDefault();
+ if (guess.trim() && !correctGuess && !roundEnded) {
+ socket.emit("submit-guess", {
+ roomCode,
+ user,
+ text: guess.trim(),
+ });
+ setGuess("");
+ }
+ };
+
+ const handleVote = (type) => {
+ if (!hintsUsed[type] && !roundEnded) {
+ socket.emit("vote-hint", { roomCode, uid: user.uid, hintType: type });
+ }
+ };
+
+ if (!song) {
+ return (
+
+ );
+ }
+
+if (gameOver) {
+ return (
+
+
+
+
+
Game Over!
+
+
+
+ LEADERBOARD
+
+ {leaderboard.map((p, i) => (
+
+ ))}
+
+
+
+
+
+ );
+}
+
+
+
+ return (
+
+
+
+
+ LEADERBOARD
+
+
+ {players
+ .sort((a, b) => (b.score || 0) - (a.score || 0))
+ .map((p, i) => (
+
+ ))}
+
+
+
+
+
+

+
+ IS.AI
+
+
+
+
+ {["movie", "composer", "ai"].map((type) => {
+ const icons = {
+ movie: "/film.png",
+ composer: "/music.png",
+ ai: "/AI.png",
+ };
+ const labels = {
+ movie: "REVEAL MOVIE NAME",
+ composer: "REVEAL ARTIST NAME",
+ ai: "ASK AI FOR HINT",
+ };
+
+ return (
+
+ );
+ })}
+
+
+
+
+
+
+
+
+ {showPlayButton && (
+
+
+
+)}
+
+
+
+ {roundEnded
+ ? `Round Over - Next in ${countdown}s`
+ : `Round ${round} - ${timer}s left`}
+
+
+
+
+
+

+
+ {roundEnded ? `${song.song}` : "Hidden"}
+ {/* {roundEnded && (
+ {song.movie}
+ )} */}
+
+
+
+
+
+
+

+
+

+
+
+
+
+
+
+

+
+ {hintRevealed.movie
+ ? song.movie
+ .replace(/\s*[\--]?\s*\(.*?(original motion picture soundtrack|ost|from.*?)\)/gi, "") // remove entire (Original Motion Picture Soundtrack)
+ .replace(/\s*[\--]?\s*(original motion picture soundtrack|ost|from.*)/gi, "") // fallback for non-parentheses versions
+ .replace(/\s*\)+$/, "") // remove leftover closing parenthesis
+ .trim()
+ : "Hidden"}
+
+
+
+
+
+
+
+
+

+
{hintRevealed.composer ? song?.composer : "Hidden"}
+
+
+ {hintRevealed.aiHint && (
+
+
+

+
{hintRevealed.aiHint}
+
+
+ )}
+
+
+
+
+
+
+ GUESS — BOX
+
+
+
+ {chat.map((message, i) => (
+
+ {message.system
+ ? message.text
+ : `${message.user?.name}: ${message.text}`}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default GameRoom;
\ No newline at end of file
diff --git a/client/src/pages/HomePage.jsx b/client/src/pages/HomePage.jsx
index e69de29..2782330 100644
--- a/client/src/pages/HomePage.jsx
+++ b/client/src/pages/HomePage.jsx
@@ -0,0 +1,315 @@
+import React, { useEffect, useRef, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ faRightToBracket,
+ faEarListen,
+ faBolt,
+ faTrophy,
+} from "@fortawesome/free-solid-svg-icons";
+
+const HomePage = () => {
+ const navigate = useNavigate();
+ const blobRef = useRef(null);
+ const [mousePosition, setMousePosition] = useState({
+ x: window.innerWidth / 2,
+ y: window.innerHeight / 2,
+ });
+ const [letters, setLetters] = useState(Array(8).fill(""));
+ const targetWord = "GUESSYNC";
+ const [movingBlobs, setMovingBlobs] = useState([]);
+ const containerRef = useRef(null);
+
+ const getRandomChar = () => {
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
+ return chars.charAt(Math.floor(Math.random() * chars.length));
+ };
+
+ useEffect(() => {
+ const spinDuration = 2000;
+ const letterDelay = 200;
+ const framesPerLetter = 15;
+ let timeoutIds = [];
+
+ targetWord.split("").forEach((targetLetter, index) => {
+ timeoutIds.push(
+ setTimeout(() => {
+ for (let i = 0; i < framesPerLetter; i++) {
+ timeoutIds.push(
+ setTimeout(() => {
+ setLetters((prev) => {
+ const newLetters = [...prev];
+ newLetters[index] = getRandomChar();
+ return newLetters;
+ });
+ }, i * (spinDuration / framesPerLetter))
+ );
+ }
+
+ timeoutIds.push(
+ setTimeout(() => {
+ setLetters((prev) => {
+ const newLetters = [...prev];
+ newLetters[index] = targetLetter;
+ return newLetters;
+ });
+ }, spinDuration)
+ );
+ }, index * letterDelay)
+ );
+ });
+
+ return () => timeoutIds.forEach((id) => clearTimeout(id));
+ }, []);
+
+ useEffect(() => {
+ const generateMovingBlobs = () => {
+ const newBlobs = [];
+ const colors = ["#FFFB00"];
+ const numBlobs = Math.floor(Math.random() * 40) + 20;
+
+ for (let i = 0; i < numBlobs; i++) {
+ const size = Math.random() * 60 + 80;
+ const speedX = (Math.random() * 2 - 1) * 6.5;
+ const speedY = (Math.random() * 2 - 1) * 6.5;
+ const initialX = Math.random() * (window.innerWidth - size);
+ const initialY = Math.random() * (window.innerHeight - size);
+ const opacity = Math.random() * 0.05 + 0.07;
+
+ newBlobs.push({
+ id: i,
+ size,
+ x: initialX,
+ y: initialY,
+ color: colors[0],
+ opacity,
+ speedX,
+ speedY,
+ });
+ }
+ setMovingBlobs(newBlobs);
+ };
+
+ generateMovingBlobs();
+
+ const animateBlobs = () => {
+ setMovingBlobs((prevBlobs) =>
+ prevBlobs.map((blob) => {
+ let newX = blob.x + blob.speedX;
+ let newY = blob.y + blob.speedY;
+
+ if (newX < 0 || newX > window.innerWidth - blob.size) {
+ blob.speedX *= -1;
+ newX = blob.x + blob.speedX;
+ }
+ if (newY < 0 || newY > window.innerHeight - blob.size) {
+ blob.speedY *= -1;
+ newY = blob.y + blob.speedY;
+ }
+
+ return { ...blob, x: newX, y: newY };
+ })
+ );
+ requestAnimationFrame(animateBlobs);
+ };
+
+ animateBlobs();
+ }, []);
+
+ useEffect(() => {
+ const handleMouseMove = (e) => {
+ setMousePosition({ x: e.clientX, y: e.clientY });
+ };
+ window.addEventListener("mousemove", handleMouseMove);
+ return () => window.removeEventListener("mousemove", handleMouseMove);
+ }, []);
+
+ const blobStyle = {
+ top: `${mousePosition.y - 150}px`,
+ left: `${mousePosition.x - 150}px`,
+ transform: "translate(-50%, -50%) scale(1)",
+ filter: "blur(120px)",
+ opacity: 0.26,
+ zIndex: 0,
+ position: "absolute",
+ width: "400px",
+ height: "400px",
+ borderRadius: "50%",
+ backgroundColor: "#FFFB00",
+ pointerEvents: "none",
+ animation: "morph 8s infinite alternate ease-in-out",
+ };
+
+ return (
+
+
+
+ {/* Mouse-follow blob */}
+
+
+ {/* Background floating blobs */}
+ {movingBlobs.map((blob) => (
+
+ ))}
+
+ {/* Main Content */}
+
+ {/* Logo + Title */}
+
+

+
+ {letters.map((letter, index) => (
+ = 4
+ ? "text-[#FFFB00] drop-shadow-[0_0_5px_#FFFB00]"
+ : ""
+ }`}
+ >
+ {letter}
+
+ ))}
+
+
+
+ {/* Tagline */}
+
+ Fast-paced multiplayer music guessing game.
+
+ Play with friends, guess songs in seconds, and race to the top of the leaderboard.
+
+
+
+ Listen
+
+
+
+ Guess
+
+
+
+ Win
+
+
+
+
+
+
+
+ {/* Login Button */}
+
+
+
+
+
+ );
+};
+
+export default HomePage;
\ No newline at end of file
diff --git a/client/src/pages/HowToPlay.jsx b/client/src/pages/HowToPlay.jsx
index e69de29..b71c12e 100644
--- a/client/src/pages/HowToPlay.jsx
+++ b/client/src/pages/HowToPlay.jsx
@@ -0,0 +1,224 @@
+// Added by Reshma - How To Play Page
+import Navbar from "../components/common/Navbar";
+import { useEffect, useState } from "react";
+
+const HowToPlay = () => {
+ const [visibleSteps, setVisibleSteps] = useState([]);
+
+ useEffect(() => {
+ // Staggered animation for steps
+ const timeouts = [];
+ [0, 1, 2].forEach((index) => {
+ timeouts.push(
+ setTimeout(() => {
+ setVisibleSteps(prev => [...prev, index]);
+ }, index * 300)
+ );
+ });
+
+ return () => timeouts.forEach(clearTimeout);
+ }, []);
+
+ return (
+
+
+
+
+
+ {/* Wrapper makes content centered on large screens */}
+
+
+ {/* Heading */}
+
+ How to Play
+ ?
+
+
+ {/* Steps Wrapper */}
+
+
+ {/* Step 1 */}
+
+
+ 1
+
+

+
+
+

+
+

+
+
+ Join or Create a Room
+
+
+ Enter a unique room code to join an existing match.
+
+ Generate a new room and share the code with friends.
+
+
+
+
+ {/* Step 2 */}
+
+
+ 2
+
+

+
+
+

+
+

+
+
+ Listen & Guess the Song
+
+
+ 15-second snippet of song plays with a countdown timer.
+
+ Submit your answer before time runs out.
+
+
+
+
+ {/* Step 3 */}
+
+
+ 3
+
+

+
+
+

+
+

+
+
+ See Hints & Earn Points
+
+
+ Faster guesses within 15 seconds earn more points.
+
+ If most players agree, a hint (movie/artist) is revealed.
+
+
+
+
+
+
+
+
+ );
+};
+
+export default HowToPlay;
\ No newline at end of file
diff --git a/client/src/pages/JoinRoom.jsx b/client/src/pages/JoinRoom.jsx
index e69de29..876b777 100644
--- a/client/src/pages/JoinRoom.jsx
+++ b/client/src/pages/JoinRoom.jsx
@@ -0,0 +1,213 @@
+import React, { useState, useEffect, useRef, useCallback } from "react";
+import { useNavigate } from "react-router-dom";
+import Navbar from "../components/common/Navbar";
+import socket from "../socket";
+
+export default function JoinRoom() {
+ const [code, setCode] = useState("");
+ const [error, setError] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [activeButton, setActiveButton] = useState(null);
+ const navigate = useNavigate();
+ const buttonRefs = useRef({});
+ const timeoutRef = useRef(null);
+
+ const handleDigit = useCallback(
+ (digit) => {
+ if (code.length < 6) {
+ setCode((prev) => {
+ const newCode = prev + digit;
+ // Clear error when user starts typing
+ if (error) setError("");
+ return newCode;
+ });
+ }
+ setActiveButton(digit);
+ setTimeout(() => setActiveButton(null), 150);
+ },
+ [code, error]
+ );
+
+ const handleBackspace = useCallback(() => {
+ setCode((prev) => {
+ const newCode = prev.slice(0, -1);
+ // Clear error when user starts typing
+ if (error) setError("");
+ return newCode;
+ });
+ setActiveButton("backspace");
+ setTimeout(() => setActiveButton(null), 150);
+ }, [error]);
+
+ const handleRoomUpdate = useCallback(() => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+ setIsLoading(false);
+ navigate("/waiting-room");
+ }, [navigate]);
+
+ const handleJoin = useCallback(
+ async () => {
+ if (code.length !== 6) {
+ setError("Please enter a 6-digit code");
+ return;
+ }
+
+ const user = JSON.parse(localStorage.getItem("user"));
+ const token = localStorage.getItem("token");
+
+ if (!user || !token) {
+ setError("Please sign in first");
+ navigate("/login");
+ return;
+ }
+
+ setIsLoading(true);
+ setActiveButton("join");
+ setTimeout(() => setActiveButton(null), 150);
+
+ try {
+ const headers = {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ };
+
+ const res = await fetch("https://guessync.onrender.com/api/room/join", {
+ method: "POST",
+ headers,
+ body: JSON.stringify({
+ code,
+ uid: user.uid,
+ name: user.name,
+ avatar: user.avatar,
+ }),
+ });
+
+ const data = await res.json();
+
+ if (!res.ok) {
+ throw new Error(data.message || "Failed to join room");
+ }
+
+ localStorage.setItem("room", JSON.stringify(data.room));
+ socket.auth = { uid: user.uid };
+
+ if (socket.disconnected) {
+ socket.connect();
+ }
+
+ socket.emit("join-room", { roomCode: code, user });
+ socket.once("room-updated", handleRoomUpdate);
+
+ // Set timeout for room update
+ timeoutRef.current = setTimeout(() => {
+ socket.off("room-updated", handleRoomUpdate);
+ setError("Connection timeout. Please try again.");
+ setIsLoading(false);
+ timeoutRef.current = null;
+ }, 10000);
+
+ } catch (err) {
+ console.error("Join error:", err);
+ setError(err.message || "Something went wrong. Please try again.");
+ setIsLoading(false);
+ }
+ },
+ [code, navigate, handleRoomUpdate]
+ );
+
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ if (e.key >= "0" && e.key <= "9") handleDigit(e.key);
+ else if (e.key === "Backspace") handleBackspace();
+ else if (e.key === "Enter" && !isLoading) handleJoin();
+ };
+
+ window.addEventListener("keydown", handleKeyDown);
+
+ // Cleanup function
+ return () => {
+ window.removeEventListener("keydown", handleKeyDown);
+ socket.off("room-updated", handleRoomUpdate);
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+ };
+ }, [handleJoin, isLoading]);
+
+ const getButtonActiveClass = (buttonValue) => {
+ return activeButton === buttonValue.toString()
+ ? "translate-x-[3px] translate-y-[3px] shadow-none"
+ : "";
+ };
+
+ return (
+
+
+
+
+
+
+ Enter Room Code
+
+
{code.padEnd(6, "_").split("").join(" ")}
+ {error && (
+
{error}
+ )}
+
+
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9].map((digit) => (
+
+ ))}
+
+
+
+
+
+
+
+ No Code?{" "}
+ !isLoading && navigate("/create-room")}
+ className={`font-bold ${!isLoading ? 'cursor-pointer' : 'opacity-50'} relative group inline-block`}
+ >
+ Create A New Room 🏡
+ {!isLoading && }
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/pages/LandingPage.jsx b/client/src/pages/LandingPage.jsx
index e69de29..be42b8e 100644
--- a/client/src/pages/LandingPage.jsx
+++ b/client/src/pages/LandingPage.jsx
@@ -0,0 +1,131 @@
+import { useEffect, useState } from "react";
+import { useNavigate } from "react-router-dom";
+import AvatarSelector from "../components/LandingPage/AvatarSelector";
+import GlowingButton from "../components/common/GlowingButton";
+import Navbar from "../components/common/Navbar";
+
+export default function LandingPage() {
+ const navigate = useNavigate();
+ const userData = JSON.parse(localStorage.getItem("user"));
+
+ const [name, setName] = useState(
+ userData?.name || localStorage.getItem("userName") || ""
+ );
+ const [avatar, setAvatar] = useState(
+ userData?.avatar || localStorage.getItem("userAvatar") || null
+ );
+ const [greeted, setGreeted] = useState(!!userData || (!!name && !!avatar));
+ const [isSignedUp, setIsSignedUp] = useState(!!userData);
+
+ useEffect(() => {
+ if (name.trim()) localStorage.setItem("userName", name);
+ if (avatar) localStorage.setItem("userAvatar", avatar);
+ }, [name, avatar]);
+
+ const handleEnter = () => {
+ if (name.trim() && avatar) {
+ const formatted =
+ name.charAt(0).toUpperCase() + name.slice(1).toLowerCase();
+ setName(formatted);
+ setGreeted(true);
+ localStorage.setItem("userName", formatted);
+ localStorage.setItem("userAvatar", avatar);
+ }
+ };
+
+ return (
+
+
+
+ {/* Scale landing body */}
+
+
+ {/* Greeting */}
+
+ Hello{greeted ? `, ${name}!` : "!"}
+
+
+ {/* Mobile Input & Button separated */}
+
+
+ setName(e.target.value)}
+ disabled={isSignedUp}
+ className="px-2 py-2 w-full text-sm border-2 border-black rounded-[6px] text-black placeholder-black font-silkscreen bg-white outline-black disabled:opacity-50 disabled:cursor-not-allowed"
+ />
+
+
+
+
+ {/* Desktop Input + Button inside border */}
+
+ setName(e.target.value)}
+ disabled={isSignedUp}
+ className="px-4 py-3 w-full text-base border-2 border-black rounded-[6px] text-black placeholder-black font-silkscreen bg-white outline-black disabled:opacity-50 disabled:cursor-not-allowed"
+ />
+
+
+
+ {/* Avatar Selector */}
+
+
{
+ setAvatar(a);
+ localStorage.setItem("userAvatar", a);
+ }}
+ disabled={isSignedUp}
+ />
+
+
+ {/* Join/Create Buttons */}
+
+
+
+ Have A Room-Code?
+
+
navigate("/join-room")}
+ disabled={!name.trim() || !avatar}
+ />
+
+
+
+ Want to Host a game?
+
+
navigate("/create-room")}
+ disabled={!name.trim() || !avatar}
+ />
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/src/pages/Login.jsx b/client/src/pages/Login.jsx
index e69de29..6c2ba58 100644
--- a/client/src/pages/Login.jsx
+++ b/client/src/pages/Login.jsx
@@ -0,0 +1,199 @@
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { signInWithPopup, signInWithEmailAndPassword } from "firebase/auth";
+import googleIcon from "../assets/google.svg";
+import { auth, provider } from "../config/firebase";
+import AvatarGrid from "../components/common/AvatarGrid";
+import Navbar from "../components/common/Navbar";
+
+const Login = () => {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [selectedAvatar, setSelectedAvatar] = useState(null);
+ const [errors, setErrors] = useState({});
+ const navigate = useNavigate();
+
+ const validateEmailLogin = () => {
+ const newErrors = {};
+ if (!email.trim()) newErrors.email = "Enter your email";
+ if (!password.trim()) newErrors.password = "Enter your password";
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleEmailLogin = async () => {
+ if (!validateEmailLogin()) return;
+
+ try {
+ const result = await signInWithEmailAndPassword(auth, email, password);
+ const token = await result.user.getIdToken();
+
+ localStorage.setItem("token", token);
+ localStorage.setItem(
+ "user",
+ JSON.stringify({
+ name: result.user.displayName || email,
+ uid: result.user.uid,
+ avatar: result.user.photoURL || "",
+ })
+ );
+
+ window.location.href = "/landing";
+ } catch (err) {
+ console.error("❌ Email login failed:", err.message);
+ let newErrors = {};
+
+ switch (err.code) {
+ case "auth/invalid-email":
+ newErrors.email = "Invalid email format";
+ break;
+ case "auth/user-not-found":
+ newErrors.email = "Email not registered";
+ break;
+ case "auth/wrong-password":
+ newErrors.password = "Incorrect password";
+ break;
+ default:
+ newErrors.email = "Login failed. Try again.";
+ break;
+ }
+
+ setErrors(newErrors);
+ }
+ };
+
+ const handleGoogleLogin = async () => {
+ if (!selectedAvatar) {
+ setErrors({ avatar: "Choose an avatar before continuing" });
+ return;
+ }
+
+ try {
+ const result = await signInWithPopup(auth, provider);
+ const token = await result.user.getIdToken();
+ const existingUser = JSON.parse(localStorage.getItem("user"));
+
+ localStorage.setItem("token", token);
+ localStorage.setItem(
+ "user",
+ JSON.stringify({
+ name: result.user.displayName || "Google User",
+ uid: result.user.uid,
+ avatar:
+ existingUser?.uid === result.user.uid
+ ? existingUser.avatar
+ : selectedAvatar,
+ })
+ );
+
+ window.location.href = "/landing";
+ } catch (err) {
+ console.error("❌ Google login failed:", err.message);
+ }
+ };
+
+ return (
+
+
+
+ {/* Login Form */}
+
+
+ Login
+
+
+
+
setEmail(e.target.value)}
+ />
+ {errors.email && (
+
+ {errors.email}
+
+ )}
+
+
+
setPassword(e.target.value)}
+ />
+ {errors.password && (
+
+ {errors.password}
+
+ )}
+
+
+
+
+ No Account?{" "}
+ navigate("/signup")}
+ >
+ Sign Up!
+
+
+
+
+
+
+
+
+
+ {/* Avatar Picker */}
+
+
+ PICK YOUR AVATAR
+
+
{
+ setSelectedAvatar(avatar);
+ setErrors((prev) => ({ ...prev, avatar: null }));
+ }}
+ />
+ {errors.avatar && (
+
+ {errors.avatar}
+
+ )}
+
+
+
+ );
+};
+
+export default Login;
\ No newline at end of file
diff --git a/client/src/pages/Profile.jsx b/client/src/pages/Profile.jsx
index e69de29..5222f28 100644
--- a/client/src/pages/Profile.jsx
+++ b/client/src/pages/Profile.jsx
@@ -0,0 +1,134 @@
+// Added by Varsha - Profile Page
+import React from "react";
+import { useNavigate } from "react-router-dom";
+
+const Profile = () => {
+ const navigate = useNavigate();
+
+ // Reusable tape band (now supports bottom, opacity, and z-index)
+ const TapeBand = ({ top, bottom, angle = -12, speed = 16, reverse = false, opacity = 1, z = 1 }) => {
+ const phrase = "UNDER CONSTRUCTION ⚠ WARNING ⚠";
+ const Row = () => (
+ <>
+ {Array.from({ length: 14 }).map((_, i) => (
+
+ {phrase}
+
+ ))}
+ >
+ );
+
+ const posStyle = bottom != null ? { bottom } : { top: top ?? "20%" };
+
+ return (
+
+ );
+ };
+
+ return (
+
+ {/* faint background tapes behind the text */}
+
+
+
+
+ {/* centered text content (kept) */}
+
+
+ 🚧
+
+
+ UNDER Development
+
+
+ We're cookin' something awesome here. Your profile page will be live very soon!
+
+ {/* Moved CTA below the paragraph */}
+
+
+
+ {/* bright, scrolling caution tapes at the bottom, behind the text */}
+
+
+
+
+
+ {/* Local styles for marquee + glow (unchanged) */}
+
+
+ );
+};
+
+export default Profile;
\ No newline at end of file
diff --git a/client/src/pages/Signup.jsx b/client/src/pages/Signup.jsx
index e69de29..c7372bc 100644
--- a/client/src/pages/Signup.jsx
+++ b/client/src/pages/Signup.jsx
@@ -0,0 +1,231 @@
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import googleIcon from "../assets/google.svg";
+import {
+ createUserWithEmailAndPassword,
+ updateProfile,
+ signInWithPopup,
+} from "firebase/auth";
+import { auth, provider } from "../config/firebase";
+import AvatarGrid from "../components/common/AvatarGrid";
+import Navbar from "../components/common/Navbar";
+
+const Signup = () => {
+ const [name, setName] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [selectedAvatar, setSelectedAvatar] = useState(null);
+ const [errors, setErrors] = useState({});
+ const navigate = useNavigate();
+
+ const validateFields = () => {
+ const newErrors = {};
+ if (!name.trim()) newErrors.name = "Enter your name";
+ if (!email.trim()) newErrors.email = "Enter your email";
+ if (!password.trim()) newErrors.password = "Enter your password";
+ if (!selectedAvatar) newErrors.avatar = "Choose an avatar";
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleEmailSignup = async () => {
+ if (!validateFields()) return;
+
+ try {
+ const result = await createUserWithEmailAndPassword(auth, email, password);
+ await updateProfile(result.user, { displayName: name });
+ const token = await result.user.getIdToken();
+
+ localStorage.setItem("token", token);
+ localStorage.setItem(
+ "user",
+ JSON.stringify({
+ name,
+ uid: result.user.uid,
+ avatar: selectedAvatar,
+ })
+ );
+
+ window.location.href = "/landing";
+ } catch (err) {
+ console.error("❌ Signup failed:", err.message);
+ const newErrors = {};
+
+ if (err.code === "auth/email-already-in-use") {
+ newErrors.email = "Email already in use";
+ } else if (err.code === "auth/invalid-email") {
+ newErrors.email = "Invalid email address";
+ } else if (err.code === "auth/weak-password") {
+ newErrors.password = "Password should be at least 6 characters";
+ } else {
+ newErrors.general = "Something went wrong. Try again.";
+ }
+
+ setErrors((prev) => ({ ...prev, ...newErrors }));
+ }
+ };
+
+ const handleGoogleSignup = async () => {
+ if (!selectedAvatar) {
+ setErrors({ avatar: "Choose an avatar before continuing" });
+ return;
+ }
+
+ try {
+ const result = await signInWithPopup(auth, provider);
+ const token = await result.user.getIdToken();
+
+ localStorage.setItem("token", token);
+ localStorage.setItem(
+ "user",
+ JSON.stringify({
+ name: result.user.displayName || "Google User",
+ uid: result.user.uid,
+ avatar: selectedAvatar,
+ })
+ );
+
+ window.location.href = "/landing";
+ } catch (err) {
+ console.error("Google signup failed:", err.message);
+ setErrors((prev) => ({
+ ...prev,
+ general: "Google signup failed. Try again.",
+ }));
+ }
+ };
+
+ return (
+
+
+
+ {/* Signup Form */}
+
+
+ Sign-Up
+
+
+ {/* Name */}
+
+
setName(e.target.value)}
+ />
+ {errors.name && (
+
+ {errors.name}
+
+ )}
+
+ {/* Email */}
+
+
setEmail(e.target.value)}
+ />
+ {errors.email && (
+
+ {errors.email}
+
+ )}
+
+ {/* Password */}
+
+
setPassword(e.target.value)}
+ />
+ {errors.password && (
+
+ {errors.password}
+
+ )}
+
+
+
+ {errors.general && (
+
+ {errors.general}
+
+ )}
+
+
+ Already Have an Account?{" "}
+ navigate("/login")}
+ >
+ Login!
+
+
+
+
+
+
+
+
+
+ {/* Avatar Picker */}
+
+
+ PICK YOUR AVATAR
+
+
+
{
+ setSelectedAvatar(avatar);
+ setErrors((prev) => ({ ...prev, avatar: null }));
+ }}
+ />
+
+ {errors.avatar && (
+
+ {errors.avatar}
+
+ )}
+
+
+
+ );
+};
+
+export default Signup;
\ No newline at end of file
diff --git a/client/src/pages/WaitingRoom.jsx b/client/src/pages/WaitingRoom.jsx
index e69de29..b92eebf 100644
--- a/client/src/pages/WaitingRoom.jsx
+++ b/client/src/pages/WaitingRoom.jsx
@@ -0,0 +1,125 @@
+import React, { useEffect, useState } from "react";
+import socket from "../socket";
+import { useNavigate } from "react-router-dom";
+
+const WaitingRoom = () => {
+ const [players, setPlayers] = useState([]);
+ const [roomCode, setRoomCode] = useState("");
+ const [hostUID, setHostUID] = useState("");
+ const [currentUID, setCurrentUID] = useState("");
+ const [isMovingToGame, setIsMovingToGame] = useState(false);
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const user = JSON.parse(localStorage.getItem("user"));
+ const roomData = JSON.parse(localStorage.getItem("room"));
+
+ if (!user || !roomData) {
+ navigate("/");
+ return;
+ }
+
+ setCurrentUID(user.uid);
+ setRoomCode(roomData.code);
+ setHostUID(roomData.hostUID);
+
+ const joinRoom = () => {
+ socket.emit("join-room", { roomCode: roomData.code, user });
+ };
+
+ if (!socket.connected) {
+ socket.once("connect", joinRoom);
+ if (socket.disconnected) socket.connect();
+ } else {
+ joinRoom();
+ }
+
+ socket.on("room-updated", (data) => {
+ setPlayers(data.players);
+ });
+
+ socket.on("move-to-game-room", () => {
+ setIsMovingToGame(true);
+ setTimeout(() => navigate("/game-room"), 500);
+ });
+
+ return () => {
+ socket.off("room-updated");
+ socket.off("move-to-game-room");
+ socket.off("connect");
+ };
+ }, [navigate]);
+
+ const isHost = currentUID === hostUID;
+
+ const handleStartGame = () => {
+ socket.emit("start-game", { roomCode });
+ setIsMovingToGame(true);
+ };
+
+ if (isMovingToGame) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {/* Players Section */}
+
+
+
Players ({players.length})
+
+ {players.map((p) => (
+ -
+
+
+ {p.name} {p.uid === hostUID && "👑"}
+
+
+ ))}
+
+
+
+ {isHost && (
+
+ )}
+
+
+ {/* Main Waiting Room Section */}
+
+
Waiting Room
+
+ Room Code: {roomCode}
+
+ {!isHost && (
+
+ ⏳ Waiting for host to start the game...
+
+ )}
+
+
+ {/* Chat Section */}
+
+
Chat
+
+
System: Waiting for game to start...
+
+
+
+ );
+};
+
+export default WaitingRoom;
diff --git a/client/src/socket.js b/client/src/socket.js
index e69de29..7b68557 100644
--- a/client/src/socket.js
+++ b/client/src/socket.js
@@ -0,0 +1,38 @@
+import { io } from "socket.io-client";
+
+const socket = io("https://guessync.onrender.com/", {
+ autoConnect: false,
+ reconnection: true,
+ reconnectionAttempts: Infinity,
+ reconnectionDelay: 1000,
+ reconnectionDelayMax: 5000,
+ randomizationFactor: 0.5,
+ timeout: 20000,
+
+ transports: ["websocket"],
+ upgrade: false,
+ forceNew: true,
+ withCredentials: true,
+
+ query: {
+ clientType: "player",
+ version: "1.0"
+ }
+});
+
+socket
+ .on("connect", () => {
+ console.log("⚡ Socket connected:", socket.id);
+ })
+ .on("connect_error", (err) => {
+ console.error("Connection error:", err.message);
+ setTimeout(() => socket.connect(), 1000 + Math.random() * 2000);
+ })
+ .on("reconnect_attempt", (attempt) => {
+ console.log(`Reconnect attempt #${attempt}`);
+ })
+ .on("reconnect_failed", () => {
+ console.error("Reconnection failed");
+ });
+
+export default socket;
\ No newline at end of file
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000..6f05071
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,4 @@
+[build]
+ base = "client"
+ publish = "dist"
+ command = "npm install && npm run build"
diff --git a/server/.env.example b/server/.env.example
new file mode 100644
index 0000000..165f842
--- /dev/null
+++ b/server/.env.example
@@ -0,0 +1,27 @@
+# MongoDB
+MONGO_URI="your_mongo_connection_string"
+
+# Server
+PORT=5000
+
+# Spotify API
+SPOTIFY_CLIENT_ID="your_spotify_client_id"
+SPOTIFY_CLIENT_SECRET="your_spotify_client_secret"
+
+# YouTube API
+YOUTUBE_API_KEY="your_youtube_api_key"
+
+# Gemini API
+GEMINI_API_KEY="your_gemini_api_key"
+
+# Firebase
+FIREBASE_PROJECT_ID="your_firebase_project_id"
+FIREBASE_PRIVATE_KEY_ID="your_firebase_private_key_id"
+FIREBASE_PRIVATE_KEY="your_firebase_private_key"
+FIREBASE_CLIENT_EMAIL="your_firebase_client_email"
+FIREBASE_CLIENT_ID="your_firebase_client_id"
+FIREBASE_AUTH_URI="https://accounts.google.com/o/oauth2/auth"
+FIREBASE_TOKEN_URI="https://oauth2.googleapis.com/token"
+FIREBASE_AUTH_PROVIDER_CERT_URL="https://www.googleapis.com/oauth2/v1/certs"
+FIREBASE_CLIENT_CERT_URL="your_firebase_client_cert_url"
+FIREBASE_UNIVERSE_DOMAIN="googleapis.com"
diff --git a/server/Dockerfile b/server/Dockerfile
index e69de29..54bd3d7 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -0,0 +1,12 @@
+FROM node:18
+
+WORKDIR /app
+
+COPY package*.json ./
+RUN npm install
+
+COPY . .
+
+EXPOSE 5000
+
+CMD ["npm", "start"]
diff --git a/server/config/firebase-adminsdk.js b/server/config/firebase-adminsdk.js
new file mode 100644
index 0000000..6e0deb7
--- /dev/null
+++ b/server/config/firebase-adminsdk.js
@@ -0,0 +1,16 @@
+// ✅ This file uses ESM syntax and works with process.env
+const serviceAccount = {
+ type: "service_account",
+ project_id: process.env.FIREBASE_PROJECT_ID,
+ private_key_id: process.env.FIREBASE_PRIVATE_KEY_ID,
+ private_key: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n'),
+ client_email: process.env.FIREBASE_CLIENT_EMAIL,
+ client_id: process.env.FIREBASE_CLIENT_ID,
+ auth_uri: process.env.FIREBASE_AUTH_URI,
+ token_uri: process.env.FIREBASE_TOKEN_URI,
+ auth_provider_x509_cert_url: process.env.FIREBASE_AUTH_PROVIDER_CERT_URL,
+ client_x509_cert_url: process.env.FIREBASE_CLIENT_CERT_URL,
+ universe_domain: process.env.FIREBASE_UNIVERSE_DOMAIN,
+};
+
+export default serviceAccount;
diff --git a/server/config/firebase-adminsdk.json b/server/config/firebase-adminsdk.json
deleted file mode 100644
index e69de29..0000000
diff --git a/server/config/firebase.js b/server/config/firebase.js
index e69de29..f3a4331 100644
--- a/server/config/firebase.js
+++ b/server/config/firebase.js
@@ -0,0 +1,23 @@
+import admin from 'firebase-admin';
+import serviceAccount from './firebase-adminsdk.js'; // ✅ this is now JS
+
+admin.initializeApp({
+ credential: admin.credential.cert(serviceAccount),
+});
+
+export const verifyFirebaseToken = async (req, res, next) => {
+ try {
+ const token = req.headers.authorization?.split('Bearer ')[1];
+
+ if (token === "guest") {
+ req.uid = "guest";
+ return next();
+ }
+
+ const decoded = await admin.auth().verifyIdToken(token);
+ req.uid = decoded.uid;
+ next();
+ } catch (err) {
+ return res.status(401).json({ message: 'Unauthorized' });
+ }
+};
diff --git a/server/controllers/roomController.js b/server/controllers/roomController.js
new file mode 100644
index 0000000..2dc453a
--- /dev/null
+++ b/server/controllers/roomController.js
@@ -0,0 +1,115 @@
+import Room from "../models/Room.js";
+import Song from "../models/Song.js";
+import { generateRoomCode } from "../utils/generateCode.js";
+import { getSpotifyAccessToken } from "../utils/getSpotifyAccessToken.js";
+import { getSongsFromSpotifyPlaylist } from "../utils/fillPlaylistFromSpotify.js";
+
+export const createRoom = async (req, res) => {
+ const {
+ uid,
+ name,
+ avatar,
+ useSpotify,
+ playlistId,
+ code,
+ rounds,
+ duration,
+ languages,
+ rules
+ } = req.body;
+
+ console.log("Creating room with code:", code);
+ console.log("Host:", uid, name);
+
+ try {
+ let playlist = [];
+
+ if (useSpotify && playlistId) {
+ const accessToken = await getSpotifyAccessToken();
+ playlist = await getSongsFromSpotifyPlaylist(playlistId, accessToken,rounds);
+ } else {
+ playlist = await Song.aggregate([{ $sample: { size: 10 } }]);
+ }
+
+ const newRoom = new Room({
+ code,
+ hostUID: uid,
+ players: [{ uid, name, avatar }],
+settings: {
+ rounds,
+ duration,
+ language: req.body.language || "tamil",
+},
+
+ playlist,
+ rules
+ });
+
+ console.log("Attempting to save new room...");
+ await newRoom.save();
+ console.log("Room saved to MongoDB successfully!");
+ res.status(201).json({ message: "Room created", code });
+ } catch (err) {
+ console.error("Room create error:", err);
+ res.status(500).json({ message: "Error creating room" });
+ }
+};
+
+export const joinRoom = async (req, res) => {
+ const { code, uid, name, avatar } = req.body;
+
+ console.log("Join request for room code:", code);
+ console.log("Joining user:", uid, name);
+
+ try {
+ const room = await Room.findOne({ code });
+ if (!room) {
+ console.warn("Room not found");
+ return res.status(404).json({ message: "Room not found" });
+ }
+
+ const alreadyInRoom = room.players.find((p) => p.uid === uid);
+ if (alreadyInRoom) {
+ console.warn("⚠️ Already in room");
+ return res.status(400).json({ message: "Already joined" });
+ }
+
+ room.players.push({ uid, name, avatar });
+ await room.save();
+
+
+ console.log("Player added to room and saved.");
+ res.json({ message: "Joined room", room });
+ } catch (err) {
+ console.error("Join Room error:", err);
+ res.status(500).json({ message: "Error joining room" });
+ }
+};
+
+export const updateRoom = async (req, res) => {
+ const { code } = req.params;
+ const { rounds, duration, language, rules } = req.body;
+
+ console.log("Updating room:", code);
+
+ try {
+ const room = await Room.findOne({ code });
+
+ if (!room) {
+ return res.status(404).json({ message: "Room not found" });
+ }
+
+ if (rounds !== undefined) room.settings.rounds = rounds;
+ if (duration !== undefined) room.settings.duration = duration;
+ if (language !== undefined) room.settings.language = language;
+ if (rules !== undefined) room.rules = rules;
+
+ await room.save();
+ console.log("Room updated successfully");
+
+ res.status(200).json({ message: "Room updated", room });
+ } catch (err) {
+ console.error("Room update error:", err);
+ res.status(500).json({ message: "Error updating room" });
+ }
+};
diff --git a/server/controllers/userController.js b/server/controllers/userController.js
index e69de29..788abfd 100644
--- a/server/controllers/userController.js
+++ b/server/controllers/userController.js
@@ -0,0 +1,39 @@
+import User from '../models/User.js';
+
+export const createOrUpdateUser = async (req, res) => {
+ const { uid, name, avatar } = req.body;
+
+ try {
+ let user = await User.findOne({ uid });
+
+ if (!user) {
+ user = new User({ uid, name, avatar });
+ await user.save();
+ return res.status(201).json({ message: "User created", user });
+ }
+
+ // update existing user (e.g. name/avatar changed)
+ user.name = name;
+ user.avatar = avatar;
+ await user.save();
+
+ res.json({ message: "User updated", user });
+ } catch (err) {
+ console.error("createOrUpdateUser error:", err);
+ res.status(500).json({ message: "Error saving user" });
+ }
+};
+
+export const getUserProfile = async (req, res) => {
+ const { uid } = req.params;
+
+ try {
+ const user = await User.findOne({ uid });
+ if (!user) return res.status(404).json({ message: "User not found" });
+
+ res.json({ user });
+ } catch (err) {
+ console.error("getUserProfile error:", err);
+ res.status(500).json({ message: "Error fetching user" });
+ }
+};
diff --git a/server/cronJobs/roomCleanup.js b/server/cronJobs/roomCleanup.js
index e69de29..e50cd1a 100644
--- a/server/cronJobs/roomCleanup.js
+++ b/server/cronJobs/roomCleanup.js
@@ -0,0 +1,20 @@
+// server/cronJobs/roomCleanup.js
+
+import cron from "node-cron";
+import Room from "../models/Room.js";
+
+export const scheduleRoomCleanup = () => {
+ cron.schedule("* * * * *", async () => {
+ const twentyMinsAgo = new Date(Date.now() - 20 * 60 * 1000);
+
+ try {
+ const result = await Room.deleteMany({ createdAt: { $lt: twentyMinsAgo } });
+
+ if (result.deletedCount > 0) {
+ console.log(`Deleted ${result.deletedCount} expired room at ${new Date().toLocaleTimeString()}`);
+ }
+ } catch (err) {
+ console.error("Error during room cleanup:", err.message);
+ }
+ });
+};
diff --git a/server/package-lock.json b/server/package-lock.json
index 2fbdcbd..f41b733 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -18,6 +18,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^5.1.0",
+ "express-rate-limit": "^7.5.1",
"firebase-admin": "^13.2.0",
"fuse.js": "^7.1.0",
"langchain": "^0.3.29",
@@ -1619,6 +1620,21 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/express-rate-limit": {
+ "version": "7.5.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
+ "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/express-rate-limit"
+ },
+ "peerDependencies": {
+ "express": ">= 4.11"
+ }
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
diff --git a/server/package.json b/server/package.json
index c855b49..d528533 100644
--- a/server/package.json
+++ b/server/package.json
@@ -21,6 +21,7 @@
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"express": "^5.1.0",
+ "express-rate-limit": "^7.5.1",
"firebase-admin": "^13.2.0",
"fuse.js": "^7.1.0",
"langchain": "^0.3.29",
diff --git a/server/routes/roomRoutes.js b/server/routes/roomRoutes.js
index ae91367..e951585 100644
--- a/server/routes/roomRoutes.js
+++ b/server/routes/roomRoutes.js
@@ -1,10 +1,11 @@
import express from 'express';
-import { createRoom, joinRoom } from '../controllers/roomController.js';
+import { createRoom, joinRoom,updateRoom} from '../controllers/roomController.js';
import { verifyFirebaseToken } from '../config/firebase.js';
const router = express.Router();
router.post('/create', verifyFirebaseToken, createRoom);
router.post('/join', verifyFirebaseToken, joinRoom);
+router.put('/update/:code', verifyFirebaseToken, updateRoom);
export default router;
diff --git a/server/routes/userRoutes.js b/server/routes/userRoutes.js
index e69de29..e89ecec 100644
--- a/server/routes/userRoutes.js
+++ b/server/routes/userRoutes.js
@@ -0,0 +1,9 @@
+import express from 'express';
+import { createOrUpdateUser, getUserProfile } from '../controllers/userController.js';
+
+const router = express.Router();
+
+router.post("/sync", createOrUpdateUser);
+router.get("/profile/:uid", getUserProfile);
+
+export default router;
diff --git a/server/scripts/fetchSpotifySongs.js b/server/scripts/fetchSpotifySongs.js
index e69de29..5f122da 100644
--- a/server/scripts/fetchSpotifySongs.js
+++ b/server/scripts/fetchSpotifySongs.js
@@ -0,0 +1,41 @@
+import fetch from 'node-fetch';
+import getYoutubeVideoId from './getYoutubeVideoId.js';
+
+export async function getSongsFromSpotifyPlaylist(playlistId, accessToken) {
+ let songs = [];
+ let nextUrl = `https://api.spotify.com/v1/playlists/${playlistId}/tracks?limit=100`;
+
+ while (nextUrl) {
+ const res = await fetch(nextUrl, {
+ headers: { Authorization: `Bearer ${accessToken}` }
+ });
+
+ const data = await res.json();
+ const items = data.items || [];
+
+ for (let item of items) {
+ const track = item.track;
+ if (!track) continue;
+
+ const songName = track.name;
+ const artistName = track.artists?.[0]?.name || "";
+ const composer = track.artists.map(a => a.name).join(", ");
+ const movie = track.album.name;
+ const cover = track.album.images?.[0]?.url || "";
+
+ const videoId = await getYoutubeVideoId(`${songName} ${artistName}`);
+
+ songs.push({
+ song: songName,
+ movie,
+ composer,
+ cover,
+ videoId
+ });
+ }
+
+ nextUrl = data.next;
+ }
+ const shuffled = songs.sort(() => 0.5 - Math.random());
+ return shuffled.slice(0, 10);
+}
diff --git a/server/scripts/fetchYouTubeIds.js b/server/scripts/fetchYouTubeIds.js
index e69de29..4b896e9 100644
--- a/server/scripts/fetchYouTubeIds.js
+++ b/server/scripts/fetchYouTubeIds.js
@@ -0,0 +1,44 @@
+import dotenv from "dotenv";
+import axios from "axios";
+import mongoose from "mongoose";
+import Song from "../models/Song.js";
+import connectDB from "../config/db.js";
+
+dotenv.config();
+await connectDB();
+
+const API_KEY = process.env.YOUTUBE_API_KEY;
+
+const searchYouTubeVideoId = async (query) => {
+ const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&type=video&maxResults=1&q=${encodeURIComponent(
+ query
+ )}&key=${API_KEY}`;
+
+ const res = await axios.get(url);
+ return res.data.items?.[0]?.id?.videoId || null;
+};
+
+try {
+ const songs = await Song.find({ videoId: { $exists: false } });
+
+ for (const song of songs) {
+ const searchQuery = `${song.song} ${song.movie} song`;
+ console.log(`🔍 Searching for: ${searchQuery}`);
+
+ const videoId = await searchYouTubeVideoId(searchQuery);
+
+ if (videoId) {
+ song.videoId = videoId;
+ await song.save();
+ console.log(`Updated: ${song.song} → ${videoId}`);
+ } else {
+ console.warn(`No video found for: ${searchQuery}`);
+ }
+ }
+
+ console.log("YouTube videoId update done.");
+ process.exit(0);
+} catch (err) {
+ console.error(" Error fetching YouTube IDs:", err.message);
+ process.exit(1);
+}
diff --git a/server/server.js b/server/server.js
index 9678de4..c885d2c 100644
--- a/server/server.js
+++ b/server/server.js
@@ -3,6 +3,7 @@ import http from 'http';
import { Server } from 'socket.io';
import dotenv from 'dotenv';
import cors from 'cors';
+import rateLimit from 'express-rate-limit';
import connectDB from './config/db.js';
import socketHandler from './sockets/socketHandler.js';
import authRoutes from './routes/authRoutes.js';
@@ -11,25 +12,41 @@ import songRoutes from './routes/songRoutes.js';
import userRoutes from './routes/userRoutes.js';
import { scheduleRoomCleanup } from "./cronJobs/roomCleanup.js";
-
dotenv.config();
connectDB();
const app = express();
const server = http.createServer(app);
+const allowedOrigins = [
+ 'http://localhost:5173',
+ 'https://guessync.netlify.app'
+];
const io = new Server(server, {
- cors: { origin: 'http://localhost:5173' }
+ cors: {
+ origin: allowedOrigins,
+ methods: ['GET', 'POST'],
+ credentials: true
+ }
});
-
const corsOptions = {
- origin: 'http://localhost:5173',
+ origin: allowedOrigins,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
credentials: true
};
app.use(cors(corsOptions));
-
app.use(express.json());
+const apiLimiter = rateLimit({
+ windowMs: 1 * 60 * 1000,
+ max: 5,
+ message: { error: 'Too many requests, please try again in a minute.' },
+ standardHeaders: true,
+ legacyHeaders: false,
+});
+
+app.use('/api/room/create', apiLimiter);
+app.use('/api/auth/login', apiLimiter);
+
app.use('/api/auth', authRoutes);
app.use("/api/user", userRoutes);
app.use('/api/room', roomRoutes);
diff --git a/server/sockets/socketHandler.js b/server/sockets/socketHandler.js
index e69de29..9af2700 100644
--- a/server/sockets/socketHandler.js
+++ b/server/sockets/socketHandler.js
@@ -0,0 +1,226 @@
+import Room from "../models/Room.js";
+import Fuse from "fuse.js";
+import { getAIHint } from "../utils/getAIHint.js";
+
+const activeTimers = new Map();
+
+const fuzzyMatch = (actual, guess) => {
+ const fuse = new Fuse([{ name: actual }], {
+ keys: ["name"],
+ includeScore: true,
+ threshold: 0.4,
+ });
+ return fuse.search(guess).length > 0 && fuse.search(guess)[0].score <= 0.4;
+};
+
+const socketHandler = (io) => {
+ io.on("connection", (socket) => {
+ console.log(`User connected: ${socket.id}`);
+
+ socket.on("join-room", async ({ roomCode, user }) => {
+ try {
+ await socket.join(roomCode);
+ console.log(`${socket.id} joined room ${roomCode}`);
+
+ let room = await Room.findOne({ code: roomCode });
+ if (!room) {
+ socket.emit("room-not-found");
+ return;
+ }
+
+ const alreadyJoined = room.players.find((p) => p.uid === user.uid);
+ if (!alreadyJoined) {
+ room.players.push({ ...user, score: 0 });
+ await room.save();
+ console.log(`+ ${user.name} added to room ${roomCode}`);
+ }
+
+ process.nextTick(() => {
+ io.to(roomCode).emit("room-updated", {
+ players: room.players,
+ hintsUsed: room.hintUsed || {},
+ });
+ });
+ } catch (err) {
+ console.error("Join-room error:", err);
+ }
+ });
+
+ socket.on("start-game", async ({ roomCode }) => {
+ try {
+ const room = await Room.findOne({ code: roomCode });
+ if (!room) return;
+
+ room.isActive = true;
+ room.currentRound = 1;
+ room.currentSongIndex = 0;
+ room.guesses = [];
+ room.guessedCorrectlyThisRound = [];
+ room.hintVotes = { movie: [], composer: [], cover: [], ai: [] };
+ room.hintUsed = { movie: false, composer: false, cover: false, ai: false };
+ await room.save();
+
+ io.to(roomCode).emit("move-to-game-room");
+
+ setTimeout(() => startRound(roomCode, io), 3000);
+ } catch (err) {
+ console.error("Start-game error:", err);
+ }
+ });
+
+ socket.on("submit-guess", async ({ roomCode, user, text }) => {
+ try {
+ const room = await Room.findOne({ code: roomCode });
+ if (!room || !room.isActive) return;
+
+ const song = room.playlist[room.currentSongIndex];
+ const correct = fuzzyMatch(song.song.trim().toLowerCase(), text.trim().toLowerCase());
+
+ room.guesses.push({ uid: user.uid, text, correct });
+ const player = room.players.find((p) => p.uid === user.uid);
+
+ if (correct && player) {
+ player.score += 10;
+ if (!room.guessedCorrectlyThisRound.includes(user.uid)) {
+ room.guessedCorrectlyThisRound.push(user.uid);
+ }
+ await room.save();
+ io.to(roomCode).emit("correct-guess", { user, score: player.score });
+
+ if (room.guessedCorrectlyThisRound.length === room.players.length) {
+ endRound(roomCode, io);
+ }
+ } else {
+ await room.save();
+ io.to(roomCode).emit("new-guess", { user, text });
+ }
+ } catch (err) {
+ console.error("Submit-guess error:", err);
+ }
+ });
+
+ socket.on("vote-hint", async ({ roomCode, uid, hintType }) => {
+ try {
+ const room = await Room.findOne({ code: roomCode });
+ if (!room || !room.isActive) return;
+
+ room.hintVotes ??= { movie: [], composer: [], cover: [], ai: [] };
+ room.hintUsed ??= { movie: false, composer: false, cover: false, ai: false };
+
+ if (room.hintUsed[hintType] || room.hintVotes[hintType].includes(uid)) return;
+
+ room.hintVotes[hintType].push(uid);
+ await room.save();
+
+ const totalPlayers = room.players.length;
+ const votes = room.hintVotes[hintType].length;
+
+ io.to(roomCode).emit("hint-vote-count", { hintType, votes, totalPlayers });
+
+ if (votes >= Math.ceil(totalPlayers / 2)) {
+ room.hintUsed[hintType] = true;
+ await room.save();
+
+ if (hintType === "ai") {
+ const aiHint = await getAIHint(room.playlist[room.currentSongIndex]?.song);
+ io.to(roomCode).emit("reveal-hint", { hintType: "ai", aiHint });
+ } else {
+ io.to(roomCode).emit("reveal-hint", { hintType });
+ }
+ }
+ } catch (err) {
+ console.error("Vote-hint error:", err);
+ }
+ });
+
+ socket.on("disconnect", () => {
+ console.log(`User disconnected: ${socket.id}`);
+ });
+ });
+};
+
+const startRound = async (roomCode, io) => {
+ try {
+ const room = await Room.findOne({ code: roomCode });
+ if (!room) return;
+
+ room.guessedCorrectlyThisRound = [];
+ await room.save();
+
+ const song = room.playlist[room.currentSongIndex];
+ io.to(roomCode).emit("start-round", {
+ round: room.currentRound,
+ song,
+ duration: room.settings.duration,
+ players: room.players,
+ });
+
+ activeTimers.set(
+ roomCode,
+ setTimeout(() => endRound(roomCode, io), room.settings.duration * 1000)
+ );
+ } catch (err) {
+ console.error("Start-round error:", err);
+ }
+};
+
+const endRound = async (roomCode, io) => {
+ try {
+ const room = await Room.findOne({ code: roomCode });
+ if (!room) return;
+
+ clearTimeout(activeTimers.get(roomCode));
+ activeTimers.delete(roomCode);
+
+ io.to(roomCode).emit("round-ended");
+ io.to(roomCode).emit("loading-next-round");
+
+ const nextSongIndex = room.currentSongIndex + 1;
+ if (
+ nextSongIndex < room.playlist.length &&
+ room.currentRound < room.settings.rounds
+ ) {
+ const nextSong = room.playlist[nextSongIndex];
+ io.to(roomCode).emit("preload-next-song", {
+ song: nextSong,
+ duration: room.settings.duration,
+ });
+ }
+
+ activeTimers.set(
+ roomCode,
+ setTimeout(() => nextRound(roomCode, io), 5000)
+ );
+ } catch (err) {
+ console.error("End-round error:", err);
+ }
+};
+
+const nextRound = async (roomCode, io) => {
+ try {
+ const room = await Room.findOne({ code: roomCode });
+ if (!room) return;
+
+ room.currentSongIndex += 1;
+ room.currentRound += 1;
+ room.guesses = [];
+ room.guessedCorrectlyThisRound = [];
+ room.hintVotes = { movie: [], composer: [], cover: [], ai: [] };
+ room.hintUsed = { movie: false, composer: false, cover: false, ai: false };
+
+ if (room.currentSongIndex >= room.playlist.length || room.currentRound > room.settings.rounds) {
+ await room.save();
+ io.to(roomCode).emit("game-over", {
+ leaderboard: room.players.sort((a, b) => b.score - a.score),
+ });
+ return;
+ }
+
+ await room.save();
+ startRound(roomCode, io);
+ } catch (err) {
+ console.error("Next-round error:", err);
+ }
+};
+
+export default socketHandler;
diff --git a/server/utils/fillPlaylistFromSpotify.js b/server/utils/fillPlaylistFromSpotify.js
index e69de29..c799a60 100644
--- a/server/utils/fillPlaylistFromSpotify.js
+++ b/server/utils/fillPlaylistFromSpotify.js
@@ -0,0 +1,56 @@
+import fetch from 'node-fetch';
+import getYoutubeVideoId from './getYoutubeVideoId.js';
+
+function shuffleArray(array) {
+ return array
+ .map(value => ({ value, sort: Math.random() }))
+ .sort((a, b) => a.sort - b.sort)
+ .map(({ value }) => value);
+}
+
+export async function getSongsFromSpotifyPlaylist(playlistId, accessToken, count = 15) {
+ const allTracks = [];
+ let offset = 0;
+ const limit = 100;
+
+ while (true) {
+ const url = `https://api.spotify.com/v1/playlists/${playlistId}/tracks?limit=${limit}&offset=${offset}`;
+ const res = await fetch(url, {
+ headers: { Authorization: `Bearer ${accessToken}` }
+ });
+
+ const data = await res.json();
+ const items = data.items || [];
+
+ allTracks.push(...items);
+
+ if (items.length < limit) break;
+
+ offset += limit;
+ }
+
+ const shuffled = shuffleArray(allTracks).slice(0, count);
+
+ const songs = await Promise.all(
+ shuffled.map(async (item) => {
+ const track = item.track;
+ const songName = track.name;
+ const artistName = track.artists?.[0]?.name || "";
+ const composer = track.artists.map(a => a.name).join(", ");
+ const movie = track.album.name;
+ const cover = track.album.images?.[0]?.url || "";
+
+ const videoId = await getYoutubeVideoId(`${songName} ${artistName}`);
+
+ return {
+ song: songName,
+ movie,
+ composer,
+ cover,
+ videoId
+ };
+ })
+ );
+
+ return songs;
+}
diff --git a/server/utils/generateCode.js b/server/utils/generateCode.js
index e69de29..130edf0 100644
--- a/server/utils/generateCode.js
+++ b/server/utils/generateCode.js
@@ -0,0 +1,4 @@
+export const generateRoomCode = () => {
+ return Math.floor(100000 + Math.random() * 900000).toString();
+ };
+
\ No newline at end of file
diff --git a/server/utils/getAIHint.js b/server/utils/getAIHint.js
index e69de29..284029b 100644
--- a/server/utils/getAIHint.js
+++ b/server/utils/getAIHint.js
@@ -0,0 +1,27 @@
+import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
+import { PromptTemplate } from "@langchain/core/prompts";
+
+export const getAIHint = async (songName) => {
+ try {
+ const model = new ChatGoogleGenerativeAI({
+ apiKey: process.env.GEMINI_API_KEY,
+ model: "gemini-1.5-flash",
+ temperature: 0.7,
+ });
+
+ const promptTemplate = PromptTemplate.fromTemplate(
+ `Give me a fun, short, and helpful hint for the song '{songName}'.
+Avoid mentioning the title or giving it away directly. The hint should make it much easier to guess`
+ );
+
+ const formattedPrompt = await promptTemplate.format({ songName: songName });
+ const result = await model.invoke(formattedPrompt);
+
+ const hint = result.content;
+
+ return hint.trim();
+ } catch (err) {
+ console.error("Error generating AI hint:", err.message);
+ return "Couldn't fetch a hint right now. Try again later.";
+ }
+};
\ No newline at end of file
diff --git a/server/utils/getSpotifyAccessToken.js b/server/utils/getSpotifyAccessToken.js
index e69de29..eb1799f 100644
--- a/server/utils/getSpotifyAccessToken.js
+++ b/server/utils/getSpotifyAccessToken.js
@@ -0,0 +1,37 @@
+
+import fetch from 'node-fetch';
+
+let cachedToken = null;
+let expiresAt = 0;
+
+export async function getSpotifyAccessToken() {
+ const now = Date.now();
+
+ if (cachedToken && now < expiresAt) {
+ return cachedToken;
+ }
+
+ const clientId = process.env.SPOTIFY_CLIENT_ID;
+ const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
+
+ const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
+
+ const res = await fetch("https://accounts.spotify.com/api/token", {
+ method: "POST",
+ headers: {
+ Authorization: `Basic ${credentials}`,
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ body: "grant_type=client_credentials"
+ });
+
+ const data = await res.json();
+
+ if (data.access_token) {
+ cachedToken = data.access_token;
+ expiresAt = now + (data.expires_in * 1000) - 60000;
+ return cachedToken;
+ } else {
+ throw new Error("Failed to fetch Spotify access token");
+ }
+}
diff --git a/server/utils/getYoutubeVideoId.js b/server/utils/getYoutubeVideoId.js
index e69de29..2f4bd41 100644
--- a/server/utils/getYoutubeVideoId.js
+++ b/server/utils/getYoutubeVideoId.js
@@ -0,0 +1,14 @@
+import fetch from 'node-fetch';
+
+export default async function getYoutubeVideoId(query) {
+ const apiKey = process.env.YOUTUBE_API_KEY;
+ const encodedQuery = encodeURIComponent(query);
+
+ const url = `https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodedQuery}&key=${apiKey}&type=video&maxResults=1`;
+
+ const res = await fetch(url);
+ const data = await res.json();
+
+ const videoId = data.items?.[0]?.id?.videoId || null;
+ return videoId;
+}