diff --git a/backend/index.js b/backend/index.js index 35c4912..d424910 100644 --- a/backend/index.js +++ b/backend/index.js @@ -11,7 +11,7 @@ const auth = require("./middleware/auth"); app.use( cors({ origin: ["http://localhost:5173"], - methods: ["GET", "POST"], + methods: ["GET", "POST", "PUT", "DELETE"], credentials: true, }) ); @@ -30,11 +30,13 @@ const handleRoom = require("./socket/socketHandler"); const submissionRoutes = require("./routes/submission"); const authRoutes = require("./routes/Auth"); const dashboardRoutes = require("./routes/dashboard"); +const questionRoutes = require("./routes/question"); // API Routes app.use("/api/submissions", auth, submissionRoutes); app.use("/api", authRoutes); app.use("/api/dashboard", dashboardRoutes); +app.use("/api/questions", questionRoutes); // Create HTTP server const server = http.createServer(app); diff --git a/backend/routes/question.js b/backend/routes/question.js index bbc5042..f1f284b 100644 --- a/backend/routes/question.js +++ b/backend/routes/question.js @@ -1,5 +1,3 @@ -// routes/questionRoutes.js - const express = require("express"); const router = express.Router(); const Question = require("../models/Question"); @@ -7,42 +5,78 @@ const runCode = require("../controllers/runCodeController"); router.post("/run", runCode); +// +// CREATE QUESTION +// +router.post("/", async (req, res) => { + try { + const question = new Question(req.body); + await question.save(); + res.json(question); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// +// GET ALL QUESTIONS +// +router.get("/", async (req, res) => { + try { + const { difficulty, tag } = req.query; + + let filter = {}; + if (difficulty) filter.difficulty = difficulty; + if (tag) filter.tags = tag; + + const questions = await Question.find(filter); + return res.json(questions || []); + } catch (err) { + return res.status(500).json({ error: err.message }); + } +}); + +// +// GET RANDOM QUESTION +// router.get("/random", async (req, res) => { try { const count = await Question.countDocuments(); + if (count === 0) { - return res.status(404).json({ message: "No questions found" }); + return res.json(null); } const random = Math.floor(Math.random() * count); const question = await Question.findOne().skip(random); - res.json(question); + return res.json(question || null); } catch (err) { - res.status(500).json({ error: err.message }); + return res.status(500).json({ error: err.message }); } }); + +// router.get("/:id", async (req, res) => { try { const question = await Question.findById(req.params.id); - res.json(question); + return res.json(question || null); } catch (err) { - res.status(500).json({ error: err.message }); + return res.status(500).json({ error: err.message }); } }); -router.get("/", async (req, res) => { - const { difficulty, tag } = req.query; - - let filter = {}; - - if (difficulty) filter.difficulty = difficulty; - if (tag) filter.tags = tag; - - const questions = await Question.find(filter); - - res.json(questions); +// +// DELETE +// +router.delete("/:id", async (req, res) => { + try { + await Question.findByIdAndDelete(req.params.id); + res.json({ message: "Deleted successfully" }); + } catch (err) { + res.status(500).json({ error: err.message }); + } }); module.exports = router; \ No newline at end of file diff --git a/backend/seed/questions.seed.js b/backend/seed/questions.seed.js new file mode 100644 index 0000000..ec92a26 --- /dev/null +++ b/backend/seed/questions.seed.js @@ -0,0 +1,207 @@ +const mongoose = require("mongoose"); +const Question = require("../models/Question"); + +mongoose.connect("mongodb://localhost:27017/interviewDB"); + +const questions = [ + // ================= EASY (1–20) ================= + { + title: "Two Sum", + description: "Find two numbers that add up to target", + difficulty: "easy", + tags: ["array", "hashmap"], + constraints: "O(n)", + examples: [{ input: "nums=[2,7,11,15], target=9", output: "[0,1]", explanation: "2+7=9" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "2,7,11,15,9", output: "[0,1]" }] + }, + { + title: "Reverse String", + description: "Reverse a string", + difficulty: "easy", + tags: ["string"], + constraints: "O(1) space", + examples: [{ input: "hello", output: "olleh", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "hello", output: "olleh" }] + }, + { + title: "Valid Parentheses", + description: "Check valid brackets", + difficulty: "easy", + tags: ["stack"], + constraints: "O(n)", + examples: [{ input: "()[]{}", output: "true", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "()[]{}", output: "true" }] + }, + { + title: "Palindrome Number", + description: "Check if number is palindrome", + difficulty: "easy", + tags: ["math"], + constraints: "O(log n)", + examples: [{ input: "121", output: "true", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "121", output: "true" }] + }, + { + title: "Best Time to Buy and Sell Stock", + description: "Max profit from stock prices", + difficulty: "easy", + tags: ["array", "greedy"], + constraints: "O(n)", + examples: [{ input: "[7,1,5,3,6,4]", output: "5", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "7,1,5,3,6,4", output: "5" }] + }, + + // (continue EASY until 20) + { + title: "Contains Duplicate", + description: "Check duplicates in array", + difficulty: "easy", + tags: ["array"], + constraints: "O(n)", + examples: [{ input: "[1,2,3,1]", output: "true", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "1,2,3,1", output: "true" }] + }, + { + title: "Single Number", + description: "Find element appearing once", + difficulty: "easy", + tags: ["bit manipulation"], + constraints: "O(n)", + examples: [{ input: "[2,2,1]", output: "1", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "2,2,1", output: "1" }] + }, + { + title: "Merge Two Sorted Lists", + description: "Merge linked lists", + difficulty: "easy", + tags: ["linked list"], + constraints: "O(n)", + examples: [{ input: "1->2, 1->3", output: "1->1->2->3", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "", output: "" }] + }, + { + title: "Valid Anagram", + description: "Check if strings are anagrams", + difficulty: "easy", + tags: ["string"], + constraints: "O(n)", + examples: [{ input: "anagram, nagaram", output: "true", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "anagram,nagaram", output: "true" }] + }, + { + title: "Binary Search", + description: "Search element in sorted array", + difficulty: "easy", + tags: ["search"], + constraints: "O(log n)", + examples: [{ input: "[1,2,3,4], target=3", output: "2", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "1,2,3,4,3", output: "2" }] + }, + + // ================= MEDIUM (21–40) ================= + { + title: "3Sum", + description: "Find triplets with zero sum", + difficulty: "medium", + tags: ["array", "two pointers"], + constraints: "O(n^2)", + examples: [{ input: "[-1,0,1,2,-1,-4]", output: "[[-1,-1,2],[-1,0,1]]", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "-1,0,1,2,-1,-4", output: "" }] + }, + { + title: "Container With Most Water", + description: "Max water container", + difficulty: "medium", + tags: ["two pointers"], + constraints: "O(n)", + examples: [{ input: "[1,8,6,2,5,4,8,3,7]", output: "49", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "1,8,6,2,5,4,8,3,7", output: "49" }] + }, + { + title: "Longest Substring Without Repeating Characters", + description: "Find longest substring", + difficulty: "medium", + tags: ["string", "sliding window"], + constraints: "O(n)", + examples: [{ input: "abcabcbb", output: "3", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "abcabcbb", output: "3" }] + }, + { + title: "Group Anagrams", + description: "Group similar anagrams", + difficulty: "medium", + tags: ["hashmap"], + constraints: "O(nk log k)", + examples: [{ input: "eat,tea,tan", output: "[[eat,tea],[tan]]", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "eat,tea,tan", output: "" }] + }, + { + title: "Search in Rotated Sorted Array", + description: "Search in rotated array", + difficulty: "medium", + tags: ["binary search"], + constraints: "O(log n)", + examples: [{ input: "[4,5,6,7,0,1,2], target=0", output: "4", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "4,5,6,7,0,1,2,0", output: "4" }] + }, + + // ================= HARD (41–50) ================= + { + title: "Median of Two Sorted Arrays", + description: "Find median", + difficulty: "hard", + tags: ["binary search"], + constraints: "O(log n)", + examples: [{ input: "[1,3],[2]", output: "2", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "", output: "" }] + }, + { + title: "Trapping Rain Water", + description: "Calculate trapped water", + difficulty: "hard", + tags: ["two pointers"], + constraints: "O(n)", + examples: [{ input: "[0,1,0,2]", output: "1", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "0,1,0,2", output: "1" }] + }, + { + title: "Merge k Sorted Lists", + description: "Merge multiple lists", + difficulty: "hard", + tags: ["heap"], + constraints: "O(n log k)", + examples: [{ input: "k lists", output: "merged list", explanation: "" }], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [{ input: "", output: "" }] + } +]; + +const seedDB = async () => { + try { + await Question.deleteMany({}); + await Question.insertMany(questions); + console.log("50 Questions seeded successfully"); + mongoose.connection.close(); + } catch (err) { + console.error(err); + } +}; + +seedDB(); \ No newline at end of file diff --git a/backend/socket/socketHandler.js b/backend/socket/socketHandler.js index ec34e5d..cdfdf63 100644 --- a/backend/socket/socketHandler.js +++ b/backend/socket/socketHandler.js @@ -6,20 +6,24 @@ let timers = {}; let intervals = {}; module.exports = (socket, io) => { - - //BLOCK UNAUTHORIZED SOCKETS + + // Ensure user exists if (!socket.user) { socket.disconnect(); return; } - const getUsername = () => socket.user?.username || "Guest"; + const getUsername = () => { + const email = socket.user?.username || "Guest"; + return email.split("@")[0]; // 🔥 remove domain +}; + console.log("SOCKET CONNECTED:", socket.id, getUsername()); // ================= CREATE ROOM ================= socket.on("create-room", ({ mode }, callback) => { const roomId = Math.random().toString(36).substring(2, 8); - rooms[roomId] = { + const room = { users: [ { socketId: socket.id, @@ -30,13 +34,17 @@ module.exports = (socket, io) => { status: "waiting", }; + rooms[roomId] = room; + socket.join(roomId); + console.log(`${getUsername()} created room ${roomId}`); + if (callback) callback(roomId); io.in(roomId).emit("room-users", { - users: rooms[roomId].users, - mode: rooms[roomId].mode, + users: room.users, + mode: room.mode, roomId, }); }); @@ -82,11 +90,15 @@ module.exports = (socket, io) => { // ================= CHAT ================= socket.on("send-message", ({ roomId, message }) => { - io.in(roomId).emit("receive-message", { + if (!message?.trim()) return; + + const msgData = { message, - sender: getUsername(), + sender: getUsername(), time: new Date().toLocaleTimeString(), - }); + }; + + io.in(roomId).emit("receive-message", msgData); }); // ================= TIMER ================= @@ -123,7 +135,8 @@ module.exports = (socket, io) => { socket.emit("code-output", output); } } catch (err) { - console.error(err); + console.error("RUN CODE ERROR:", err); + const msg = "Error running code"; if (roomId) { @@ -158,8 +171,19 @@ module.exports = (socket, io) => { io.in(roomId).emit("interview-ended"); }); + // ================= QUESTION SYNC ================= + socket.on("question-change", ({ roomId, question }) => { + if (!roomId) return; + + console.log(`${getUsername()} changed question in ${roomId}`); + + socket.to(roomId).emit("question-change", question); + }); + // ================= DISCONNECT ================= socket.on("disconnect", () => { + console.log(`User disconnected: ${socket.id}`); + for (let roomId in rooms) { const room = rooms[roomId]; @@ -175,6 +199,7 @@ module.exports = (socket, io) => { } delete rooms[roomId]; + console.log(`Room deleted: ${roomId}`); } else { io.in(roomId).emit("room-users", { users: room.users, @@ -183,7 +208,5 @@ module.exports = (socket, io) => { }); } } - - console.log(`User disconnected: ${socket.id}`); }); }; \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cab05a7..d8962de 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,21 +16,37 @@ "react-dom": "^19.2.4", "react-router-dom": "^7.14.0", "recharts": "^3.8.1", - "socket.io-client": "^4.8.3", - "tailwindcss": "^4.2.2" + "socket.io-client": "^4.8.3" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.2.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "postcss": "^8.5.10", + "tailwindcss": "^4.2.2", "vite": "^8.0.1" } }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1153,6 +1169,20 @@ "node": ">= 20" } }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", + "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "postcss": "^8.5.6", + "tailwindcss": "4.2.2" + } + }, "node_modules/@tailwindcss/vite": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", @@ -1384,6 +1414,43 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, "node_modules/axios": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", @@ -2223,6 +2290,20 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2494,6 +2575,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "license": "MIT", + "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -3080,9 +3162,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.9", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", - "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "funding": [ { "type": "opencollective", @@ -3098,6 +3180,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3107,6 +3190,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0453292..3100e65 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,18 +18,21 @@ "react-dom": "^19.2.4", "react-router-dom": "^7.14.0", "recharts": "^3.8.1", - "socket.io-client": "^4.8.3", - "tailwindcss": "^4.2.2" + "socket.io-client": "^4.8.3" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@tailwindcss/postcss": "^4.2.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.5.0", "eslint": "^9.39.4", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "postcss": "^8.5.10", + "tailwindcss": "^4.2.2", "vite": "^8.0.1" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index fbe14a4..fe451e5 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,6 @@ export default { plugins: { - tailwindcss: {}, + "@tailwindcss/postcss": {}, autoprefixer: {}, }, }; \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4d4958e..7ad8dab 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,20 +1,22 @@ -import { BrowserRouter, Routes, Route, Navigate, useNavigate } from "react-router-dom"; +import { Routes, Route, Navigate } from "react-router-dom"; import { useEffect } from "react"; import axios from "axios"; import { socket, connectSocket } from "./services/socket"; +import { useNavigate } from "react-router-dom"; +// pages... import Login from "./pages/Login"; import Register from "./pages/Register"; import ForgotPassword from "./pages/Forgotpassword"; import ResetPassword from "./pages/ResetPassword"; import Resources from "./pages/Resources"; - import SelectMode from "./pages/SelectMode"; import RoomEntry from "./pages/RoomEntry"; import Lobby from "./pages/Lobby"; import Interview from "./pages/Interview"; import SoloInterview from "./pages/SoloInterview"; import Dashboard from "./pages/Dashboard"; + import AdminPanel from "./pages/AdminPanel"; function setAuthToken() { const token = localStorage.getItem("token"); @@ -30,10 +32,6 @@ function PrivateRoute({ children }) { return token ? children : ; } -function AppWrapper() { - return ; -} - function App() { const navigate = useNavigate(); @@ -41,7 +39,9 @@ function App() { setAuthToken(); const token = localStorage.getItem("token"); - if (token) connectSocket(); + if (token) { + connectSocket(); + } const interceptor = axios.interceptors.response.use( (res) => res, @@ -73,7 +73,10 @@ function App() {

Welcome

-
@@ -86,15 +89,74 @@ function App() { } /> } /> -
} /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + +} /> ); } -export default AppWrapper; \ No newline at end of file +export default App; \ No newline at end of file diff --git a/frontend/src/components/InterviewRoom/QuestionPanel.jsx b/frontend/src/components/InterviewRoom/QuestionPanel.jsx index c8be417..5499c56 100644 --- a/frontend/src/components/InterviewRoom/QuestionPanel.jsx +++ b/frontend/src/components/InterviewRoom/QuestionPanel.jsx @@ -1,101 +1,150 @@ -import { useState, useEffect } from "react"; -import { getRandomQuestion } from "../../services/questionService"; - -export default function QuestionPanel({ socket, roomId, isHost, mode, setQuestion: setParentQuestion }) { - const [question, setQuestion] = useState(null); +import { useEffect, useState } from "react"; +import { getAllQuestions } from "../../services/questionAPI"; + +export default function QuestionPanel({ + socket, + roomId, + isHost, + mode, + setQuestion: setParentQuestion, +}) { + const [questions, setQuestions] = useState([]); + const [selected, setSelected] = useState(null); const [activeTab, setActiveTab] = useState("description"); const [language, setLanguage] = useState("cpp"); - const handleNewQuestion = async () => { - const q = await getRandomQuestion(); - - setQuestion(q); - setParentQuestion && setParentQuestion(q); // ✅ sync with parent - + // + // ✅ LOAD QUESTION BANK + // + useEffect(() => { + const fetch = async () => { + const data = await getAllQuestions(); + setQuestions(Array.isArray(data) ? data : []); + }; + + fetch(); + }, []); + + // + // ✅ HANDLE SELECT QUESTION + // + const handleSelect = (q) => { + setSelected(q); + setParentQuestion?.(q); + + // sync in peer mode if (mode === "peer" && isHost && socket) { socket.emit("question-change", { roomId, question: q }); } }; + // + // ✅ SOCKET LISTENER (RECEIVE QUESTION) + // useEffect(() => { if (mode === "peer" && socket) { const handler = (q) => { - setQuestion(q); - setParentQuestion && setParentQuestion(q); // ✅ sync here also + setSelected(q); + setParentQuestion?.(q); }; socket.on("question-change", handler); - return () => socket.off("question-change", handler); } }, [socket, mode]); return ( -
- - {(mode === "solo" || isHost) && ( - - )} - - {question && ( - <> -

{question.title}

- -

- Difficulty: {question.difficulty} +

+ + {/* LEFT - QUESTION LIST */} +
+

Questions

+ + {questions.length === 0 ? ( +

No questions available

+ ) : ( + questions.map((q) => ( +
handleSelect(q)} + className={`p-2 mb-2 rounded cursor-pointer border ${ + selected?._id === q._id ? "bg-blue-100" : "hover:bg-gray-100" + }`} + > +

{q.title}

+

{q.difficulty}

+
+ )) + )} +
+ + {/* RIGHT - QUESTION DETAILS */} +
+ + {!selected ? ( +

+ Select a question from left panel

- -
- {["description", "examples", "constraints"].map((tab) => ( - - ))} -
- -
- {activeTab === "description" &&

{question.description}

} - - {activeTab === "constraints" && ( -

{question.constraints}

- )} - - {activeTab === "examples" && ( -
- {question.examples.map((ex, i) => ( + ) : ( + <> +

{selected.title}

+ +

+ Difficulty: {selected.difficulty} +

+ + {/* tabs */} +
+ {["description", "examples", "constraints"].map((tab) => ( + + ))} +
+ +
+ {activeTab === "description" && ( +

{selected.description}

+ )} + + {activeTab === "constraints" && ( +

{selected.constraints}

+ )} + + {activeTab === "examples" && ( + selected.examples?.map((ex, i) => (

Input: {ex.input}

Output: {ex.output}

Explanation: {ex.explanation}

- ))} -
- )} -
- -
- -
- -
-            {question.starterCode?.[language]}
-          
- - )} + )) + )} +
+ + {/* starter code */} +
+ + +
+                {selected.starterCode?.[language]}
+              
+
+ + )} +
); } \ No newline at end of file diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 286154e..f749347 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -1,10 +1,11 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import { BrowserRouter } from 'react-router-dom' -import App from './App' +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import { BrowserRouter } from "react-router-dom"; +import "./index.css"; -ReactDOM.createRoot(document.getElementById('root')).render( +ReactDOM.createRoot(document.getElementById("root")).render( -) \ No newline at end of file +); \ No newline at end of file diff --git a/frontend/src/pages/AdminPanel.jsx b/frontend/src/pages/AdminPanel.jsx new file mode 100644 index 0000000..bb00357 --- /dev/null +++ b/frontend/src/pages/AdminPanel.jsx @@ -0,0 +1,142 @@ +import { useEffect, useState } from "react"; +import { + getAllQuestions, + createQuestion, + deleteQuestion, +} from "../services/questionAPI"; + +export default function AdminPanel() { + const [questions, setQuestions] = useState([]); + + const [form, setForm] = useState({ + title: "", + description: "", + difficulty: "easy", + tags: "", + }); + + // LOAD QUESTIONS + const loadQuestions = async () => { + const data = await getAllQuestions(); + setQuestions(Array.isArray(data) ? data : []); + }; + + useEffect(() => { + loadQuestions(); + }, []); + + // HANDLE INPUT + const handleChange = (e) => { + setForm({ ...form, [e.target.name]: e.target.value }); + }; + + // ADD QUESTION + const handleSubmit = async (e) => { + e.preventDefault(); + + const payload = { + ...form, + tags: form.tags.split(",").map((t) => t.trim()), + examples: [], + starterCode: { cpp: "", java: "", python: "" }, + testCases: [], + }; + + await createQuestion(payload); + + setForm({ + title: "", + description: "", + difficulty: "easy", + tags: "", + }); + + loadQuestions(); + }; + + // DELETE + const handleDelete = async (id) => { + await deleteQuestion(id); + loadQuestions(); + }; + + return ( +
+ +

Admin Panel

+ + {/* ADD FORM */} +
+

Add Question

+ + + +