From eedf1140fc527337ef682a4f25e833228f83e8d6 Mon Sep 17 00:00:00 2001 From: Shyam Sundar Sharma Date: Fri, 10 Jul 2026 21:42:43 +0530 Subject: [PATCH 1/2] Add new feature --- client/package-lock.json | 90 ++++++- client/package.json | 3 +- client/src/App.jsx | 212 ++++++++++++++- server/index.js | 225 +++++++++++++++- server/node_modules/.package-lock.json | 338 ++++++++++++++++++++++++ server/package-lock.json | 343 ++++++++++++++++++++++++- server/package.json | 5 +- 7 files changed, 1202 insertions(+), 14 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 7218f0be6..5e2d05a9d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,7 +10,8 @@ "dependencies": { "axios": "^1.13.6", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@eslint/js": "^9.39.4", @@ -856,6 +857,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -1205,7 +1212,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1266,6 +1272,28 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2318,7 +2346,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2623,6 +2650,34 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2843,6 +2898,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/client/package.json b/client/package.json index d3eb99ad5..03b6b5c13 100644 --- a/client/package.json +++ b/client/package.json @@ -12,7 +12,8 @@ "dependencies": { "axios": "^1.13.6", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "socket.io-client": "^4.8.3" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/client/src/App.jsx b/client/src/App.jsx index 2f59dcac5..fb1fb96fd 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -22,6 +22,7 @@ */ import { useEffect, useState, useRef, useMemo } from 'react' +import { io } from 'socket.io-client' import './App.css' // API base URL from environment variables (Vite) @@ -556,6 +557,7 @@ function NumPad({ value, onChange, onSubmit, disabled, showDecimal, showSlash, s {showCaret && } {showX && } + {onSubmit && } ) @@ -35458,9 +35460,191 @@ function TenthApp({ onBack }) { ) } +// ─── Multiplayer Battle App ────────────────────────────────────────────────── +function BattleApp({ onBack, topic }) { + const [socket, setSocket] = useState(null); + const [status, setStatus] = useState('waiting'); // menu, waiting, playing, finished + const [roomId, setRoomId] = useState(null); + const [players, setPlayers] = useState({}); + const [question, setQuestion] = useState(null); + const [answer, setAnswer] = useState(''); + const [winner, setWinner] = useState(null); + const [isIncorrect, setIsIncorrect] = useState(false); + const [selectedTopic, setSelectedTopic] = useState('tatsavit'); + + useEffect(() => { + const backendUrl = API || window.location.origin; + const s = io(backendUrl); + setSocket(s); + + s.on('connect', () => { + console.log('Connected to battle server', s.id); + s.emit('join_match', { topic: topic || 'tatsavit' }); + }); + + s.on('match_found', (data) => { + setRoomId(data.roomId); + setStatus('playing'); + }); + + s.on('new_question', (q) => { + setQuestion(q); + setAnswer(''); + setIsIncorrect(false); + }); + + s.on('score_update', (p) => { + setPlayers(p); + }); + + s.on('answer_incorrect', () => { + setIsIncorrect(true); + setTimeout(() => setIsIncorrect(false), 1000); + }); + + s.on('game_over', (data) => { + setPlayers(data.players); + setWinner(data.winner); + setStatus('finished'); + }); + + s.on('opponent_disconnected', () => { + alert('Opponent disconnected!'); + setStatus('menu'); + setRoomId(null); + }); + + return () => { + s.disconnect(); + }; + }, []); + + + + const handleSubmit = (e) => { + if (e) e.preventDefault(); + if (!answer.trim() || !roomId) return; + socket.emit('submit_answer', { roomId, answer }); + }; + + let myScore = 0; + let oppScore = 0; + let oppName = 'Opponent'; + if (socket && players[socket.id]) { + myScore = players[socket.id].score; + const oppId = Object.keys(players).find(id => id !== socket.id); + if (oppId) { + oppScore = players[oppId].score; + if (oppId === 'BOT') oppName = 'Computer'; + } + } + + + + if (status === 'waiting') { + return ( + { socket.emit('disconnect'); onBack(); }}> +
+
+

Searching for opponent...

+ +
+
+ ); + } + + if (status === 'finished') { + const isWinner = winner === socket.id; + return ( + +
+

+ {isWinner ? '🏆 You Win!' : '💀 You Lose!'} +

+

Final Score: {myScore} - {oppScore}

+ +
+
+ ); + } + + return ( + +
+
+
You
+
{myScore}
+
+
+
{oppName}
+
{oppScore}
+
+
+ + {question && ( +
+ {question.display ? ( +
+ ) : ( +
{question.prompt}
+ )} +
+ )} + + {isIncorrect && ( +
+ Incorrect! Try again. +
+ )} + + {question && question.options && question.options.length > 0 ? ( + <> +
+ {question.options.map((option, idx) => { + const letter = ['A', 'B', 'C', 'D'][idx]; + return ( + + ); + })} +
+
+ +
+ + ) : ( + <> +
+ setAnswer(e.target.value)} + className="answer-input" + placeholder="Type answer..." + autoFocus + style={{ width: '200px', textAlign: 'center', fontSize: '1.5rem', padding: '0.5rem' }} + /> + +
+ + + )} + + ); +} + function App() { // Currently selected quiz mode (null = home menu, or key like 'gk', 'addition', etc.) const [mode, setMode] = useState(null) + const [battleTopic, setBattleTopic] = useState(null) // Current theme: 'dark' or 'light' // Initialized from localStorage with fallback to 'dark' @@ -35966,6 +36150,7 @@ function App() { // ========== ROUTING: MODE-BASED (HOME MENU + QUIZZES) ========== // Map quiz mode keys to their component classes const modeMap = { + battle: BattleApp, gk: GKApp, // General Knowledge addition: AdditionApp, // Basic addition quadratic: QuadraticApp, // Quadratic substitution @@ -36059,11 +36244,11 @@ function App() {
{!mode ? ( - + { setBattleTopic(topic); setMode('battle'); }} /> ) : ActiveApp ? ( - setMode(null)} /> + { setMode(null); setBattleTopic(null); }} topic={battleTopic} /> ) : ( - + { setBattleTopic(topic); setMode('battle'); }} /> )}
@@ -36078,7 +36263,9 @@ function App() { * @param {Object} props * @param {Function} props.onSelect - Callback when user selects a quiz: receives mode key (e.g., 'gk') */ -function Home({ onSelect }) { +function Home({ onSelect, onSelectBattle }) { + const [selectedApp, setSelectedApp] = useState(null); + // Special featured apps (shown in highlighted first row) const featuredApps = [ { key: 'randommix', name: 'Random Mix', subtitle: 'Adaptive cross-topic quiz', color: 'featured' }, @@ -36241,7 +36428,7 @@ function Home({ onSelect }) { padding: '6px 0', minWidth: '200px', overflow: 'hidden' }}> {featuredApps.map(app => ( - ))}
{rows} × {cols}
+ + {selectedApp && ( +
setSelectedApp(null)} style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}> +
e.stopPropagation()} style={{ background: 'var(--bg-primary)', padding: '2rem', borderRadius: '12px', textAlign: 'center', minWidth: '300px' }}> +

{selectedApp.name}

+

How would you like to play?

+
+ + +
+
+
+ )} ) } diff --git a/server/index.js b/server/index.js index d4e3f341a..f1adfa455 100644 --- a/server/index.js +++ b/server/index.js @@ -47,6 +47,8 @@ const express = require('express'); const cors = require('cors'); const fs = require('fs'); const path = require('path'); +const http = require('http'); +const { Server } = require('socket.io'); // Initialize Express app and configure middleware const app = express(); @@ -8941,6 +8943,225 @@ app.get(/.*/, (_req, res) => { * Listen on all interfaces (0.0.0.0) at the configured port * 0.0.0.0 makes the server accessible from any network interface/IP address */ -app.listen(PORT, '0.0.0.0', () => { - console.log(`Tenali app running on http://0.0.0.0:${PORT}`); +const server = http.createServer(app); +const io = new Server(server, { + cors: { origin: '*' } +}); + +let waitingPlayers = {}; +let waitingTimeouts = {}; +const activeRooms = {}; + +async function generateBattleQuestion(topic) { + if (topic === 'tatsavit' || !topic) { + const qLevel = Math.floor(Math.random() * 9); + return tatsavitQuestion(1, qLevel); + } + + try { + const res = await fetch(`http://127.0.0.1:${process.env.PORT || 4000}/${topic}-api/question`); + if (!res.ok) throw new Error('API Error'); + const q = await res.json(); + + let prompt = q.prompt || q.display || q.question; + let answer = q.answer; + + if (topic === 'simul' && q.eqs && q.solution) { + prompt = q.eqs.map(eq => `${eq.a}x ${eq.b >= 0 ? '+' : '-'} ${Math.abs(eq.b)}y = ${eq.d}`).join('
'); + answer = `x=${q.solution.x},y=${q.solution.y}`; + } + + if (!prompt) prompt = "Solve: " + JSON.stringify(q); + + if (q.options && q.options.length > 0) { + try { + const checkRes = await fetch(`http://127.0.0.1:${process.env.PORT || 4000}/${topic}-api/check`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id: q.id, answerOption: '', solve: true }), + }); + if (checkRes.ok) { + const checkData = await checkRes.json(); + answer = checkData.correctAnswer || checkData.solution || checkData.correctAnswerText || answer; + } + } catch (err) { + console.error('Error fetching check data', err); + } + } + + if (answer === undefined) answer = q.correctAnswer || q.solution || 0; + + return { prompt, answer, display: q.display || prompt, options: q.options }; + } catch (err) { + console.error(`Error generating battle question for topic ${topic}:`, err); + const qLevel = Math.floor(Math.random() * 9); + return tatsavitQuestion(1, qLevel); + } +} + +// Helper function to handle bot logic +async function handleBotAnswer(roomId) { + const room = activeRooms[roomId]; + if (!room || !room.isBotGame) return; + + // Bot simulates correct answer + room.players['BOT'].score += 1; + io.to(roomId).emit('score_update', room.players); + + if (room.players['BOT'].score >= 10) { + io.to(roomId).emit('game_over', { winner: 'BOT', players: room.players }); + delete activeRooms[roomId]; + } else { + const nextQ = await generateBattleQuestion(room.topic); + if (!activeRooms[roomId]) return; + room.currentQuestion = nextQ; + io.to(roomId).emit('new_question', nextQ); + + // Schedule next bot answer (4 to 8 seconds) + const delay = Math.floor(Math.random() * 4000) + 4000; + room.botTimer = setTimeout(() => handleBotAnswer(roomId), delay); + } +} + +io.on('connection', (socket) => { + console.log('Player connected for battle:', socket.id); + + socket.on('join_match', async (data) => { + const topic = (data && data.topic) || 'tatsavit'; + + if (waitingPlayers[topic] && waitingPlayers[topic].id !== socket.id) { + if (waitingTimeouts[topic]) { + clearTimeout(waitingTimeouts[topic]); + waitingTimeouts[topic] = null; + } + const waitingPlayer = waitingPlayers[topic]; + // Match found + const roomId = `room_${waitingPlayer.id}_${socket.id}`; + socket.join(roomId); + waitingPlayer.join(roomId); + + const p1 = waitingPlayer.id; + const p2 = socket.id; + + activeRooms[roomId] = { + topic: topic, + isBotGame: false, + players: { + [p1]: { id: p1, score: 0 }, + [p2]: { id: p2, score: 0 } + }, + currentQuestion: null + }; + + const wpId = waitingPlayer.id; + waitingPlayers[topic] = null; + io.to(roomId).emit('match_found', { roomId, p1: wpId, p2: socket.id }); + + const q = await generateBattleQuestion(topic); + if (!activeRooms[roomId]) return; + activeRooms[roomId].currentQuestion = q; + io.to(roomId).emit('new_question', q); + + } else { + waitingPlayers[topic] = socket; + if (waitingTimeouts[topic]) clearTimeout(waitingTimeouts[topic]); + waitingTimeouts[topic] = setTimeout(async () => { + if (waitingPlayers[topic] && waitingPlayers[topic].id === socket.id) { + // Start a bot match + const roomId = `room_${socket.id}_BOT`; + socket.join(roomId); + + activeRooms[roomId] = { + topic: topic, + isBotGame: true, + players: { + [socket.id]: { id: socket.id, score: 0 }, + ['BOT']: { id: 'BOT', score: 0 } + }, + currentQuestion: null, + botTimer: null + }; + + const wpId = waitingPlayers[topic].id; + waitingPlayers[topic] = null; + io.to(roomId).emit('match_found', { roomId, p1: wpId, p2: 'BOT' }); + + const q = await generateBattleQuestion(topic); + if (!activeRooms[roomId]) return; + activeRooms[roomId].currentQuestion = q; + io.to(roomId).emit('new_question', q); + + const delay = Math.floor(Math.random() * 4000) + 4000; + activeRooms[roomId].botTimer = setTimeout(() => handleBotAnswer(roomId), delay); + } + }, 3000); + } + }); + + socket.on('submit_answer', async ({ roomId, answer }) => { + const room = activeRooms[roomId]; + if (!room || !room.currentQuestion) return; + + const q = room.currentQuestion; + + // Safety handling for empty answer + if (answer === undefined || answer === null) return; + const ansStr = String(answer).trim().toLowerCase(); + + const isCorrect = (typeof q.answer === 'string') + ? ansStr === q.answer.toLowerCase() || ansStr.replace(/\s+/g, '') === q.answer.toLowerCase() + : !isNaN(parseInt(ansStr, 10)) && parseInt(ansStr, 10) === q.answer; + + if (isCorrect) { + room.players[socket.id].score += 1; + + io.to(roomId).emit('score_update', room.players); + + if (room.isBotGame && room.botTimer) { + clearTimeout(room.botTimer); + } + + if (room.players[socket.id].score >= 10) { + io.to(roomId).emit('game_over', { winner: socket.id, players: room.players }); + delete activeRooms[roomId]; + } else { + const nextQ = await generateBattleQuestion(room.topic); + if (!activeRooms[roomId]) return; + room.currentQuestion = nextQ; + io.to(roomId).emit('new_question', nextQ); + + if (room.isBotGame) { + const delay = Math.floor(Math.random() * 4000) + 4000; + room.botTimer = setTimeout(() => handleBotAnswer(roomId), delay); + } + } + } else { + socket.emit('answer_incorrect'); + } + }); + + socket.on('disconnect', () => { + for (const topic in waitingPlayers) { + if (waitingPlayers[topic] && waitingPlayers[topic].id === socket.id) { + waitingPlayers[topic] = null; + if (waitingTimeouts[topic]) { + clearTimeout(waitingTimeouts[topic]); + waitingTimeouts[topic] = null; + } + } + } + for (const roomId in activeRooms) { + if (activeRooms[roomId].players[socket.id]) { + if (activeRooms[roomId].botTimer) { + clearTimeout(activeRooms[roomId].botTimer); + } + io.to(roomId).emit('opponent_disconnected'); + delete activeRooms[roomId]; + } + } + }); +}); + +server.listen(PORT, '0.0.0.0', () => { + console.log(`Tenali app running on http://0.0.0.0:${PORT} with Socket.IO`); }); diff --git a/server/node_modules/.package-lock.json b/server/node_modules/.package-lock.json index 971e47fc7..6c828a6c6 100644 --- a/server/node_modules/.package-lock.json +++ b/server/node_modules/.package-lock.json @@ -13,6 +13,21 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-proxy": { "version": "1.17.17", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", @@ -46,6 +61,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -59,6 +83,15 @@ "node": ">= 0.6" } }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -214,6 +247,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -278,6 +320,92 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -372,6 +500,29 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -425,6 +576,18 @@ } } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -933,6 +1096,44 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1256,6 +1457,105 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -1345,6 +1645,15 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -1372,6 +1681,35 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } } } } diff --git a/server/package-lock.json b/server/package-lock.json index 02550be6b..4a757dc5f 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,7 +13,10 @@ "express": "^5.1.0", "http-proxy-middleware": "^3.0.5", "jsonwebtoken": "^9.0.3", - "mongoose": "^9.6.1" + "mongoose": "^9.6.1", + "node-fetch": "^3.3.2", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3" } }, "node_modules/@mongodb-js/saslprep": { @@ -25,6 +28,21 @@ "sparse-bitfield": "^3.0.3" } }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/http-proxy": { "version": "1.17.17", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", @@ -58,6 +76,15 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -71,6 +98,15 @@ "node": ">= 0.6" } }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/bcryptjs": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", @@ -226,6 +262,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -290,6 +335,92 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -384,6 +515,29 @@ "url": "https://opencollective.com/express" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -437,6 +591,18 @@ } } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -945,6 +1111,44 @@ "node": ">= 0.6" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1268,6 +1472,105 @@ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==", "license": "MIT" }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -1357,6 +1660,15 @@ "node": ">= 0.8" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -1384,6 +1696,35 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } } } } diff --git a/server/package.json b/server/package.json index 7e9970882..3c1341a46 100644 --- a/server/package.json +++ b/server/package.json @@ -13,6 +13,9 @@ "express": "^5.1.0", "http-proxy-middleware": "^3.0.5", "jsonwebtoken": "^9.0.3", - "mongoose": "^9.6.1" + "mongoose": "^9.6.1", + "node-fetch": "^3.3.2", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3" } } From 345f8de90a2116554393557a177b70adac2f8288 Mon Sep 17 00:00:00 2001 From: Shyam Sundar Sharma Date: Sat, 11 Jul 2026 12:26:55 +0530 Subject: [PATCH 2/2] feat: add sound effects and dynamic animations --- client/package-lock.json | 11 +++ client/package.json | 1 + client/src/App.jsx | 109 +++++++++++++++++++--------- client/src/utils/audio.js | 133 +++++++++++++++++++++++++++++++++++ client/src/utils/confetti.js | 35 +++++++++ 5 files changed, 254 insertions(+), 35 deletions(-) create mode 100644 client/src/utils/audio.js create mode 100644 client/src/utils/confetti.js diff --git a/client/package-lock.json b/client/package-lock.json index 5e2d05a9d..3f769c0be 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "axios": "^1.13.6", + "canvas-confetti": "^1.9.4", "react": "^19.2.4", "react-dom": "^19.2.4", "socket.io-client": "^4.8.3" @@ -1123,6 +1124,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/client/package.json b/client/package.json index 03b6b5c13..cbbde4146 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "axios": "^1.13.6", + "canvas-confetti": "^1.9.4", "react": "^19.2.4", "react-dom": "^19.2.4", "socket.io-client": "^4.8.3" diff --git a/client/src/App.jsx b/client/src/App.jsx index fb1fb96fd..19a7e8e86 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -23,6 +23,8 @@ import { useEffect, useState, useRef, useMemo } from 'react' import { io } from 'socket.io-client' +import { playCorrectSound, playWrongSound, playWinSound, playLoseSound } from './utils/audio' +import { triggerWinConfetti } from './utils/confetti' import './App.css' // API base URL from environment variables (Vite) @@ -418,7 +420,15 @@ function ResultsTable({ results }) { * structured card with highlighted answer and formatted explanation. * For normal correct/wrong feedback, renders inline text. */ -function renderFeedback(feedback, isCorrect) { +function FeedbackComponent({ feedback, isCorrect }) { + useEffect(() => { + if (isCorrect === true) { + playCorrectSound(); + } else if (isCorrect === false && (!feedback || !feedback.startsWith('Solution:'))) { + playWrongSound(); + } + }, [isCorrect, feedback]); + if (!feedback) return null const isSolve = isCorrect === false && feedback.startsWith('Solution:') if (!isSolve) { @@ -462,6 +472,18 @@ function renderFeedback(feedback, isCorrect) { ) } +function renderFeedback(feedback, isCorrect) { + return ; +} + +function QuizCompleteEffect() { + useEffect(() => { + playWinSound(); + triggerWinConfetti(); + }, []); + return null; +} + /** * DifficultySlider — Interactive difficulty bar with draggable seeker. * Props: @@ -35472,6 +35494,17 @@ function BattleApp({ onBack, topic }) { const [isIncorrect, setIsIncorrect] = useState(false); const [selectedTopic, setSelectedTopic] = useState('tatsavit'); + useEffect(() => { + if (status === 'finished') { + if (winner === socket?.id) { + playWinSound(); + triggerWinConfetti(); + } else { + playLoseSound(); + } + } + }, [status, winner, socket]); + useEffect(() => { const backendUrl = API || window.location.origin; const s = io(backendUrl); @@ -36711,7 +36744,7 @@ function GKApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

@@ -36774,16 +36807,22 @@ function AdditionApp({ onBack }) { * Generates random a + b = ? with specified digit count */ const fetchQuestion = async (selectedDifficulty = difficulty) => { - setLoading(true) - setFeedback('') - setAnswer('') - setRevealed(false) - setIsCorrect(null) - const res = await fetch(`${API}/addition-api/question?digits=${digitMap[effectiveDiff()]}`) - const data = await res.json() - setQuestion(data) - setLoading(false) - timer.start() + try { + setLoading(true) + setFeedback('') + setAnswer('') + setRevealed(false) + setIsCorrect(null) + const res = await fetch(`${API}/addition-api/question?digits=${digitMap[effectiveDiff()]}`) + if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`) + const data = await res.json() + setQuestion(data) + setLoading(false) + timer.start() + } catch (e) { + setLoading(false) + setFeedback('Error fetching question: ' + e.message) + } } /** @@ -36956,7 +36995,7 @@ function AdditionApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete.

+ <>

Quiz complete.

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -38089,7 +38128,7 @@ function BasicArithApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete.

+ <>

Quiz complete.

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -38351,7 +38390,7 @@ function QuadraticApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete.

+ <>

Quiz complete.

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -39181,7 +39220,7 @@ function VocabApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -39488,7 +39527,7 @@ function makeMCQuizApp({ title, subtitle, apiPath, diffLabels, tip, adaptiveOnly {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

@@ -39721,7 +39760,7 @@ function makeQuizApp({ title, subtitle, apiPath, diffLabels, placeholders, tip, {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -40104,7 +40143,7 @@ function DotProdApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -41920,7 +41959,7 @@ function SquaringApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -42723,7 +42762,7 @@ function SetsApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -42876,7 +42915,7 @@ function SequencesApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -43045,7 +43084,7 @@ function RatioApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -43213,7 +43252,7 @@ function PercentApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -43433,7 +43472,7 @@ function IndicesApp({ onBack }) { {finished && (
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -43688,7 +43727,7 @@ function SurdsApp({ onBack }) { {finished && (
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -44033,7 +44072,7 @@ function FractionAddApp({ onBack }) { {/* ── Finished Phase ── */} {finished && (
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -44540,7 +44579,7 @@ function SqrtApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete.

+ <>

Quiz complete.

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -44780,7 +44819,7 @@ function PolyMulApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -45017,7 +45056,7 @@ function PolyFactorApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -45288,7 +45327,7 @@ function PrimeFactorApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -45533,7 +45572,7 @@ function QFormulaApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -45781,7 +45820,7 @@ function SimulApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -45994,7 +46033,7 @@ function FuncEvalApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} @@ -46213,7 +46252,7 @@ function LineEqApp({ onBack }) { {results.length > 0 && } } {finished &&
-

Quiz complete!

+ <>

Quiz complete!

Final score: {score}/{totalQ}

{isAdaptive &&

Reached level: {ADAPT_LABELS[curAdaptLevel]}

} diff --git a/client/src/utils/audio.js b/client/src/utils/audio.js new file mode 100644 index 000000000..4d9300bda --- /dev/null +++ b/client/src/utils/audio.js @@ -0,0 +1,133 @@ +// utils/audio.js + +// Lazy-initialize audio context to comply with browser autoplay policies +let audioCtx = null; + +function getAudioContext() { + if (!audioCtx) { + audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + } + // If the context is suspended (due to autoplay policy), try to resume it + if (audioCtx.state === 'suspended') { + audioCtx.resume(); + } + return audioCtx; +} + +/** + * Play a satisfying high-pitched "ding" for correct answers + */ +export function playCorrectSound() { + try { + const ctx = getAudioContext(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'sine'; + osc.frequency.setValueAtTime(880, ctx.currentTime); // A5 + osc.frequency.exponentialRampToValueAtTime(1760, ctx.currentTime + 0.1); // Slide up to A6 + + gain.gain.setValueAtTime(0, ctx.currentTime); + gain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 0.05); + gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.5); + + osc.connect(gain); + gain.connect(ctx.destination); + + osc.start(); + osc.stop(ctx.currentTime + 0.5); + } catch (err) { + console.error('Audio playback failed', err); + } +} + +/** + * Play a low buzz for incorrect answers + */ +export function playWrongSound() { + try { + const ctx = getAudioContext(); + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'sawtooth'; + osc.frequency.setValueAtTime(150, ctx.currentTime); + osc.frequency.exponentialRampToValueAtTime(100, ctx.currentTime + 0.3); + + gain.gain.setValueAtTime(0, ctx.currentTime); + gain.gain.linearRampToValueAtTime(0.3, ctx.currentTime + 0.05); + gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3); + + osc.connect(gain); + gain.connect(ctx.destination); + + osc.start(); + osc.stop(ctx.currentTime + 0.3); + } catch (err) { + console.error('Audio playback failed', err); + } +} + +/** + * Play an ascending fanfare for winning + */ +export function playWinSound() { + try { + const ctx = getAudioContext(); + const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6 + + notes.forEach((freq, i) => { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'triangle'; + const startTime = ctx.currentTime + i * 0.15; + + osc.frequency.setValueAtTime(freq, startTime); + + gain.gain.setValueAtTime(0, startTime); + gain.gain.linearRampToValueAtTime(0.4, startTime + 0.05); + gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.8); + + osc.connect(gain); + gain.connect(ctx.destination); + + osc.start(startTime); + osc.stop(startTime + 0.8); + }); + } catch (err) { + console.error('Audio playback failed', err); + } +} + +/** + * Play a descending sad tune for losing + */ +export function playLoseSound() { + try { + const ctx = getAudioContext(); + const notes = [392.00, 370.00, 349.23, 329.63]; // G4, Gb4, F4, E4 + + notes.forEach((freq, i) => { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.type = 'square'; + const startTime = ctx.currentTime + i * 0.3; + + osc.frequency.setValueAtTime(freq, startTime); + + gain.gain.setValueAtTime(0, startTime); + gain.gain.linearRampToValueAtTime(0.2, startTime + 0.1); + gain.gain.exponentialRampToValueAtTime(0.01, startTime + 0.6); + + osc.connect(gain); + gain.connect(ctx.destination); + + osc.start(startTime); + osc.stop(startTime + 0.6); + }); + } catch (err) { + console.error('Audio playback failed', err); + } +} diff --git a/client/src/utils/confetti.js b/client/src/utils/confetti.js new file mode 100644 index 000000000..f998713aa --- /dev/null +++ b/client/src/utils/confetti.js @@ -0,0 +1,35 @@ +import confetti from 'canvas-confetti'; + +/** + * Triggers a celebratory confetti burst effect. + */ +export function triggerWinConfetti() { + const duration = 3 * 1000; + const animationEnd = Date.now() + duration; + const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 10000 }; + + function randomInRange(min, max) { + return Math.random() * (max - min) + min; + } + + const interval = setInterval(function() { + const timeLeft = animationEnd - Date.now(); + + if (timeLeft <= 0) { + return clearInterval(interval); + } + + const particleCount = 50 * (timeLeft / duration); + // since particles fall down, start a bit higher than random + confetti({ + ...defaults, + particleCount, + origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } + }); + confetti({ + ...defaults, + particleCount, + origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } + }); + }, 250); +}