Commit 482c9d87 authored by TokaKaram's avatar TokaKaram

refactor quiz flow

parent 51309db4
...@@ -42,11 +42,13 @@ exports.getQuizById = async (req, res) => { ...@@ -42,11 +42,13 @@ exports.getQuizById = async (req, res) => {
exports.startQuiz = async (req, res) => { exports.startQuiz = async (req, res) => {
try { try {
// console.log("Start Quiz");
const [existing] = await pool.execute( const [existing] = await pool.execute(
'SELECT * FROM quiz_attempts WHERE user_id = ? AND quiz_id = ? AND status = "in_progress"', 'SELECT * FROM quiz_attempts WHERE user_id = ? AND quiz_id = ? AND status = "in_progress"',
[req.user.id, req.params.id] [req.user.id, req.params.id]
); );
if (existing.length > 0) { if (existing.length > 0) {
// console.log("existing", existing);
return res.json({ return res.json({
attemptId: existing[0].id, attemptId: existing[0].id,
message: "Continuing existing attempt", message: "Continuing existing attempt",
...@@ -56,6 +58,7 @@ exports.startQuiz = async (req, res) => { ...@@ -56,6 +58,7 @@ exports.startQuiz = async (req, res) => {
"SELECT COUNT(*) as count FROM quiz_scenarios WHERE quiz_id = ?", "SELECT COUNT(*) as count FROM quiz_scenarios WHERE quiz_id = ?",
[req.params.id] [req.params.id]
); );
// console.log("secenarios", scenarios[0].count);
const [result] = await pool.execute( const [result] = await pool.execute(
"INSERT INTO quiz_attempts (user_id, quiz_id, total_questions) VALUES (?, ?, ?)", "INSERT INTO quiz_attempts (user_id, quiz_id, total_questions) VALUES (?, ?, ?)",
[req.user.id, req.params.id, scenarios[0].count] [req.user.id, req.params.id, scenarios[0].count]
...@@ -69,6 +72,7 @@ exports.startQuiz = async (req, res) => { ...@@ -69,6 +72,7 @@ exports.startQuiz = async (req, res) => {
exports.submitAnswer = async (req, res) => { exports.submitAnswer = async (req, res) => {
try { try {
const { attemptId, scenarioId, selectedAnswer } = req.body; const { attemptId, scenarioId, selectedAnswer } = req.body;
// console.log("req.body", req.body);
const [scenarios] = await pool.execute( const [scenarios] = await pool.execute(
"SELECT * FROM scenarios WHERE id = ?", "SELECT * FROM scenarios WHERE id = ?",
[scenarioId] [scenarioId]
...@@ -86,7 +90,12 @@ exports.submitAnswer = async (req, res) => { ...@@ -86,7 +90,12 @@ exports.submitAnswer = async (req, res) => {
"SELECT answers FROM quiz_attempts WHERE id = ?", "SELECT answers FROM quiz_attempts WHERE id = ?",
[attemptId] [attemptId]
); );
let answers = attempt[0].answers ? JSON.parse(attempt[0].answers) : {};
let answers = attempt[0].answers
? typeof attempt[0].answers === "string"
? JSON.parse(attempt[0].answers)
: attempt[0].answers
: {};
answers[scenarioId] = { selectedAnswer, isCorrect }; answers[scenarioId] = { selectedAnswer, isCorrect };
const correctCount = Object.values(answers).filter( const correctCount = Object.values(answers).filter(
(a) => a.isCorrect (a) => a.isCorrect
...@@ -101,6 +110,7 @@ exports.submitAnswer = async (req, res) => { ...@@ -101,6 +110,7 @@ exports.submitAnswer = async (req, res) => {
rationale: scenario.best_answer_rationale, rationale: scenario.best_answer_rationale,
}); });
} catch (error) { } catch (error) {
console.log("error", error);
res.status(500).json({ message: "Server error", error: error.message }); res.status(500).json({ message: "Server error", error: error.message });
} }
}; };
...@@ -108,6 +118,7 @@ exports.submitAnswer = async (req, res) => { ...@@ -108,6 +118,7 @@ exports.submitAnswer = async (req, res) => {
exports.completeQuiz = async (req, res) => { exports.completeQuiz = async (req, res) => {
try { try {
const { attemptId, timeTaken } = req.body; const { attemptId, timeTaken } = req.body;
// console.log("req.body", req.body);
const [attempts] = await pool.execute( const [attempts] = await pool.execute(
"SELECT * FROM quiz_attempts WHERE id = ? AND user_id = ?", "SELECT * FROM quiz_attempts WHERE id = ? AND user_id = ?",
[attemptId, req.user.id] [attemptId, req.user.id]
...@@ -116,6 +127,9 @@ exports.completeQuiz = async (req, res) => { ...@@ -116,6 +127,9 @@ exports.completeQuiz = async (req, res) => {
return res.status(404).json({ message: "Attempt not found" }); return res.status(404).json({ message: "Attempt not found" });
} }
const attempt = attempts[0]; const attempt = attempts[0];
// console.log("attempt", attempt);
// console.log("attempt.correct_answers=>", attempt.correct_answers);
// console.log(" attempt.total_questions=>", attempt.total_questions);
const score = Math.round( const score = Math.round(
(attempt.correct_answers / attempt.total_questions) * 100 (attempt.correct_answers / attempt.total_questions) * 100
); );
...@@ -133,6 +147,7 @@ exports.completeQuiz = async (req, res) => { ...@@ -133,6 +147,7 @@ exports.completeQuiz = async (req, res) => {
totalQuestions: attempt.total_questions, totalQuestions: attempt.total_questions,
}); });
} catch (error) { } catch (error) {
console.log("erroir", error);
res.status(500).json({ message: "Server error", error: error.message }); res.status(500).json({ message: "Server error", error: error.message });
} }
}; };
......
...@@ -57,6 +57,7 @@ exports.getHistory = async (req, res) => { ...@@ -57,6 +57,7 @@ exports.getHistory = async (req, res) => {
FROM quiz_attempts qa FROM quiz_attempts qa
JOIN quizzes q ON qa.quiz_id = q.id JOIN quizzes q ON qa.quiz_id = q.id
WHERE qa.user_id = ? WHERE qa.user_id = ?
AND qa.total_questions > 0
ORDER BY qa.started_at DESC ORDER BY qa.started_at DESC
`, `,
[req.user.id] [req.user.id]
......
...@@ -3,16 +3,16 @@ const pool = require("../config/database"); ...@@ -3,16 +3,16 @@ const pool = require("../config/database");
module.exports = async (req, res, next) => { module.exports = async (req, res, next) => {
try { try {
console.log("token", req.header("Authorization")); // console.log("token", req.header("Authorization"));
const token = req.header("Authorization")?.replace("Bearer ", ""); const token = req.header("Authorization")?.replace("Bearer ", "");
console.log("token without bearer", token); // console.log("token without bearer", token);
if (!token) { if (!token) {
return res return res
.status(401) .status(401)
.json({ message: "No token, authorization denied" }); .json({ message: "No token, authorization denied" });
} }
const decoded = jwt.verify(token, process.env.JWT_SECRET); const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log("decoded", decoded); // console.log("decoded", decoded);
const [users] = await pool.execute( const [users] = await pool.execute(
"SELECT id, username, email, role FROM users WHERE id = ?", "SELECT id, username, email, role FROM users WHERE id = ?",
[decoded.id] [decoded.id]
...@@ -23,7 +23,7 @@ module.exports = async (req, res, next) => { ...@@ -23,7 +23,7 @@ module.exports = async (req, res, next) => {
req.user = users[0]; req.user = users[0];
next(); next();
} catch (error) { } catch (error) {
console.log("error", error); // console.log("error", error);
res.status(401).json({ message: "Token is not valid error" }); res.status(401).json({ message: "Token is not valid error" });
} }
}; };
import { NavLink, useNavigate } from 'react-router-dom'; import { NavLink, useNavigate } from 'react-router-dom';
import { useAuth } from '../../context/AuthContext'; import { useAuth } from '../../context/AuthContext';
import { FiHome, FiUser, FiLogOut, FiAward, FiClock, FiSettings } from 'react-icons/fi'; import { FiX ,FiMenu,FiHome, FiUser, FiLogOut, FiAward, FiClock, FiSettings } from 'react-icons/fi';
import { useState } from 'react';
export default function Navbar() { export default function Navbar() {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => setIsOpen(!isOpen);
const navLinkClass = ({ isActive }) =>
`flex items-center gap-2 transition px-4 py-2 rounded-md ${
isActive ? 'text-indigo-600 bg-indigo-50 lg:bg-transparent' : 'text-gray-600 hover:text-indigo-600 hover:bg-gray-50 lg:hover:bg-transparent'
}`;
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
...@@ -10,43 +19,117 @@ export default function Navbar() { ...@@ -10,43 +19,117 @@ export default function Navbar() {
logout(); logout();
navigate('/login'); navigate('/login');
}; };
return (
return ( <nav className="bg-white shadow-lg relative z-50">
<nav className="bg-white shadow-lg">
<div className="container mx-auto px-4"> <div className="container mx-auto px-4">
<div className="flex justify-between items-center h-16"> <div className="flex justify-between items-center h-16">
<NavLink to="/" className="text-2xl font-bold text-indigo-600">MCQ App</NavLink> {/* Logo */}
{user ? ( <NavLink to="/" className="text-2xl font-bold text-indigo-600">
<div className="flex items-center gap-6"> MCQ App
<NavLink to="/" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')} >
<FiHome /> Quizzes
</NavLink>
<NavLink to="/leaderboard" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
<FiAward /> Leaderboard
</NavLink>
<NavLink to="/history" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
<FiClock /> History
</NavLink>
{user.role === 'admin' && (
<NavLink to="/admin" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
<FiSettings /> Admin
</NavLink> </NavLink>
)}
<NavLink to="/profile" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}> {/* Hamburger Button (Visible on mobile/tablet) */}
<FiUser /> {user.username} <div className="lg:hidden">
</NavLink> <button onClick={toggleMenu} className="text-gray-600 focus:outline-none p-2">
<button onClick={handleLogout} className="flex items-center gap-2 text-red-500 hover:text-red-700 transition"> {isOpen ? <FiX size={24} /> : <FiMenu size={24} />}
<FiLogOut /> Logout
</button> </button>
</div> </div>
) : (
<div className="flex items-center gap-4"> {/* Desktop Menu */}
<NavLink to="/login" className="text-gray-600 hover:text-indigo-600 transition">Login</NavLink> <div className="hidden lg:flex items-center gap-6">
<NavLink to="/register" className="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition">Sign Up</NavLink> <NavContent user={user} handleLogout={handleLogout} navLinkClass={navLinkClass} />
</div>
</div>
{/* Mobile & Tablet Menu (Dropdown) */}
<div
className={`lg:hidden overflow-hidden transition-all duration-300 ease-in-out ${
isOpen ? 'max-h-screen border-t py-4' : 'max-h-0'
}`}
>
<div className="flex flex-col gap-2">
<NavContent
user={user}
handleLogout={handleLogout}
navLinkClass={navLinkClass}
isMobile={true}
closeMenu={() => setIsOpen(false)}
/>
</div> </div>
)}
</div> </div>
</div> </div>
</nav> </nav>
); );
};
// مكون داخلي لتجنب تكرار الروابط
const NavContent = ({ user, handleLogout, navLinkClass, isMobile, closeMenu }) => {
if (!user) {
return (
<div className={`flex ${isMobile ? 'flex-col gap-2' : 'items-center gap-4'}`}>
<NavLink to="/login" onClick={closeMenu} className="text-gray-600 hover:text-indigo-600 px-4 py-2">Login</NavLink>
<NavLink to="/register" onClick={closeMenu} className="bg-indigo-600 text-white px-4 py-2 rounded-lg text-center">Sign Up</NavLink>
</div>
);
}
return (
<>
<NavLink to="/" onClick={closeMenu} className={navLinkClass}><FiHome /> Quizzes</NavLink>
<NavLink to="/leaderboard" onClick={closeMenu} className={navLinkClass}><FiAward /> Leaderboard</NavLink>
<NavLink to="/history" onClick={closeMenu} className={navLinkClass}><FiClock /> History</NavLink>
{user.role === 'admin' && (
<NavLink to="/admin" onClick={closeMenu} className={navLinkClass}><FiSettings /> Admin</NavLink>
)}
<NavLink to="/profile" onClick={closeMenu} className={navLinkClass}><FiUser /> {user.username}</NavLink>
<button
onClick={() => { handleLogout(); if(isMobile) closeMenu(); }}
className="flex items-center gap-2 text-red-500 hover:text-red-700 transition px-4 py-2"
>
<FiLogOut /> Logout
</button>
</>
);
// return (
// <nav className="bg-white shadow-lg">
// <div className="container mx-auto px-4">
// <div className="flex justify-between items-center h-16">
// <NavLink to="/" className="text-2xl font-bold text-indigo-600">MCQ App</NavLink>
// {user ? (
// <div className="flex items-center gap-6">
// <NavLink to="/" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')} >
// <FiHome /> Quizzes
// </NavLink>
// <NavLink to="/leaderboard" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiAward /> Leaderboard
// </NavLink>
// <NavLink to="/history" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiClock /> History
// </NavLink>
// {user.role === 'admin' && (
// <NavLink to="/admin" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiSettings /> Admin
// </NavLink>
// )}
// <NavLink to="/profile" className={({ isActive }) => (isActive ? 'text-indigo-600 flex items-center gap-2 hover:text-indigo-600 transition' : 'flex items-center gap-2 text-gray-600 hover:text-indigo-600 transition')}>
// <FiUser /> {user.username}
// </NavLink>
// <button onClick={handleLogout} className="flex items-center gap-2 text-red-500 hover:text-red-700 transition">
// <FiLogOut /> Logout
// </button>
// </div>
// ) : (
// <div className="flex items-center gap-4">
// <NavLink to="/login" className="text-gray-600 hover:text-indigo-600 transition">Login</NavLink>
// <NavLink to="/register" className="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition">Sign Up</NavLink>
// </div>
// )}
// </div>
// </div>
// </nav>
// );
} }
\ No newline at end of file
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
export default function QuestionCard({ scenario, selectedAnswer, onSelectAnswer, showResult, answerResult }) { export default function QuestionCard({ scenario, selectedAnswer, onSelectAnswer, showResult, answerResult }) {
const givensTable = typeof scenario.givens_table === 'string' ? JSON.parse(scenario.givens_table) : scenario.givens_table; // console.log("giventtable=>", scenario);
const givensTable = typeof scenario?.givens_table === 'string' ? JSON.parse(scenario.givens_table) : scenario.givens_table;
const options = useMemo(() =>{ const options = useMemo(() =>{
return[ return[
......
...@@ -24,6 +24,7 @@ export default function QuizTake() { ...@@ -24,6 +24,7 @@ export default function QuizTake() {
const loadQuiz = async () => { const loadQuiz = async () => {
try { try {
const quizRes = await api.get(`/quiz/${id}`); const quizRes = await api.get(`/quiz/${id}`);
console.log("quizRes", quizRes);
setQuiz(quizRes.data); setQuiz(quizRes.data);
const startRes = await api.post(`/quiz/${id}/start`); const startRes = await api.post(`/quiz/${id}/start`);
setAttemptId(startRes.data.attemptId); setAttemptId(startRes.data.attemptId);
...@@ -75,21 +76,23 @@ export default function QuizTake() { ...@@ -75,21 +76,23 @@ export default function QuizTake() {
if (loading) return <Loading />; if (loading) return <Loading />;
if (!quiz) return null; if (!quiz) return null;
const currentScenario = quiz.scenarios[currentIndex]; const currentScenario = quiz?.scenarios[currentIndex];
const progress = ((currentIndex + 1) / quiz.scenarios.length) * 100; const progress = ((currentIndex + 1) / quiz?.scenarios.length) * 100;
return ( return (
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="bg-white rounded-xl shadow-lg p-6 mb-6"> <div className="bg-white rounded-xl shadow-lg p-6 mb-6">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h1 className="text-2xl font-bold text-gray-800">{quiz.title}</h1> <h1 className="text-2xl font-bold text-gray-800">{quiz.title}</h1>
<span className="text-gray-500">Question {currentIndex + 1} of {quiz.scenarios.length}</span> <span className="text-gray-500">Question { quiz.scenarios.length === 0 ? 0 : currentIndex + 1} of {quiz.scenarios.length}</span>
</div> </div>
<div className="w-full bg-gray-200 rounded-full h-2"> <div className="w-full bg-gray-200 rounded-full h-2">
<div className="bg-indigo-600 h-2 rounded-full transition-all duration-300" style={{ width: `${progress}%` }}></div> <div className="bg-indigo-600 h-2 rounded-full transition-all duration-300" style={{ width: `${progress}%` }}></div>
</div> </div>
</div> </div>
<QuestionCard scenario={currentScenario} selectedAnswer={selectedAnswer} onSelectAnswer={setSelectedAnswer} showResult={showResult} answerResult={answerResult} /> {quiz.scenarios.length === 0 ? <div className="bg-white rounded-xl shadow-lg p-6 mb-6">No questions found for this quiz.</div>
: <QuestionCard scenario={currentScenario} selectedAnswer={selectedAnswer} onSelectAnswer={setSelectedAnswer} showResult={showResult} answerResult={answerResult} />}
<div className="mt-6 flex justify-end"> <div className="mt-6 flex justify-end">
{!showResult ? ( {!showResult ? (
<button onClick={handleSubmitAnswer} disabled={submitting || !selectedAnswer} className="bg-indigo-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-indigo-700 transition flex items-center gap-2 disabled:opacity-50"> <button onClick={handleSubmitAnswer} disabled={submitting || !selectedAnswer} className="bg-indigo-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-indigo-700 transition flex items-center gap-2 disabled:opacity-50">
......
...@@ -16,7 +16,7 @@ export default function Login() { ...@@ -16,7 +16,7 @@ export default function Login() {
setLoading(true); setLoading(true);
try { try {
const r=await login(email, password); const r=await login(email, password);
console.log("r=>",r); // console.log("r=>",r);
toast.success('Login successful!'); toast.success('Login successful!');
if(r.user.role === 'admin') if(r.user.role === 'admin')
navigate('/admin'); navigate('/admin');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment