242 lines
7.9 KiB
TypeScript
242 lines
7.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter, useParams } from "next/navigation";
|
|
import DarkThemeLayout from "../../components/DarkThemeLayout";
|
|
import Navbar from "../../components/Navbar";
|
|
import QuizQuestion from "../../components/QuizQuestion";
|
|
|
|
interface User {
|
|
username: string;
|
|
points: number;
|
|
}
|
|
|
|
interface Quiz {
|
|
id: string;
|
|
title: string;
|
|
description: string;
|
|
questions: Array<{
|
|
id: string;
|
|
text: string;
|
|
options: string[];
|
|
correctAnswer: number;
|
|
}>;
|
|
pointsPerQuestion: number;
|
|
}
|
|
|
|
export default function QuizPage() {
|
|
const router = useRouter();
|
|
const params = useParams();
|
|
const quizId = params.id as string;
|
|
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [quiz, setQuiz] = useState<Quiz | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [currentQuestion, setCurrentQuestion] = useState(0);
|
|
const [answers, setAnswers] = useState<Array<{ selected: number; correct: boolean }>>([]);
|
|
const [completed, setCompleted] = useState(false);
|
|
const [totalPoints, setTotalPoints] = useState(0);
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem("token");
|
|
if (!token) {
|
|
router.push("/login");
|
|
return;
|
|
}
|
|
|
|
fetchData();
|
|
}, [router, quizId]);
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
const token = localStorage.getItem("token");
|
|
const headers = { Authorization: `Bearer ${token}` };
|
|
|
|
const [userRes, quizRes] = await Promise.all([
|
|
fetch("/api/users/me", { headers }),
|
|
fetch(`/api/quizzes/${quizId}`, { headers })
|
|
]);
|
|
|
|
if (!userRes.ok || !quizRes.ok) {
|
|
throw new Error("Failed to fetch data");
|
|
}
|
|
|
|
const userData = await userRes.json();
|
|
const quizData = await quizRes.json();
|
|
|
|
if (userData.success && userData.data) {
|
|
setUser({
|
|
username: userData.data.name,
|
|
points: userData.data.pointsBalance || 0,
|
|
});
|
|
}
|
|
if (quizData.success && quizData.data) {
|
|
setQuiz(quizData.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
router.push("/tasks");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAnswer = (selectedIndex: number, isCorrect: boolean) => {
|
|
setAnswers([...answers, { selected: selectedIndex, correct: isCorrect }]);
|
|
|
|
if (isCorrect && quiz) {
|
|
setTotalPoints(totalPoints + quiz.pointsPerQuestion);
|
|
}
|
|
};
|
|
|
|
const handleNext = () => {
|
|
if (quiz && currentQuestion < quiz.questions.length - 1) {
|
|
setCurrentQuestion(currentQuestion + 1);
|
|
} else {
|
|
handleComplete();
|
|
}
|
|
};
|
|
|
|
const handleComplete = async () => {
|
|
setCompleted(true);
|
|
|
|
try {
|
|
await fetch(`/api/quizzes/${quizId}/complete`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
answers,
|
|
points: totalPoints
|
|
})
|
|
});
|
|
|
|
if (user) {
|
|
setUser({ ...user, points: user.points + totalPoints });
|
|
}
|
|
} catch (error) {
|
|
console.error("Error completing quiz:", error);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<DarkThemeLayout>
|
|
<div className="flex items-center justify-center min-h-screen">
|
|
<div className="animate-spin rounded-full h-16 w-16 border-b-2 border-yellow-500"></div>
|
|
</div>
|
|
</DarkThemeLayout>
|
|
);
|
|
}
|
|
|
|
if (!user || !quiz) {
|
|
return null;
|
|
}
|
|
|
|
const correctAnswers = answers.filter(a => a.correct).length;
|
|
const totalQuestions = quiz.questions.length;
|
|
const progressPercent = ((currentQuestion + (completed ? 1 : 0)) / totalQuestions) * 100;
|
|
|
|
if (completed) {
|
|
return (
|
|
<>
|
|
<Navbar user={user} />
|
|
<DarkThemeLayout>
|
|
<div className="max-w-3xl mx-auto space-y-8">
|
|
<div className="text-center">
|
|
<div className="inline-flex items-center justify-center w-24 h-24 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 mb-6 shadow-2xl shadow-yellow-500/50">
|
|
<svg className="w-12 h-12 text-white" fill="currentColor" viewBox="0 0 20 20">
|
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<h1 className="text-4xl font-bold text-white mb-2">Quiz Complete!</h1>
|
|
<p className="text-gray-400 text-lg">Great job completing the quiz</p>
|
|
</div>
|
|
|
|
<div className="bg-gray-800 border border-gray-700 rounded-xl p-8">
|
|
<div className="grid grid-cols-3 gap-6 mb-8">
|
|
<div className="text-center">
|
|
<p className="text-gray-400 text-sm mb-2">Score</p>
|
|
<p className="text-3xl font-bold text-white">
|
|
{correctAnswers}/{totalQuestions}
|
|
</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-gray-400 text-sm mb-2">Accuracy</p>
|
|
<p className="text-3xl font-bold text-yellow-400">
|
|
{Math.round((correctAnswers / totalQuestions) * 100)}%
|
|
</p>
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="text-gray-400 text-sm mb-2">Points Earned</p>
|
|
<p className="text-3xl font-bold text-green-400">+{totalPoints}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<button
|
|
onClick={() => router.push("/tasks")}
|
|
className="w-full bg-gradient-to-r from-yellow-500 to-orange-500 text-white font-semibold py-3 rounded-lg hover:from-yellow-600 hover:to-orange-600 transition transform hover:scale-105"
|
|
>
|
|
Back to Tasks
|
|
</button>
|
|
<button
|
|
onClick={() => router.push("/")}
|
|
className="w-full bg-gray-700 text-white font-semibold py-3 rounded-lg hover:bg-gray-600 transition"
|
|
>
|
|
Go to Dashboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</DarkThemeLayout>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Navbar user={user} />
|
|
<DarkThemeLayout>
|
|
<div className="max-w-3xl mx-auto space-y-8">
|
|
<div>
|
|
<h1 className="text-4xl font-bold text-white mb-2">{quiz.title}</h1>
|
|
<p className="text-gray-400 text-lg">{quiz.description}</p>
|
|
</div>
|
|
|
|
<div className="bg-gray-800 border border-gray-700 rounded-xl p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<span className="text-gray-400">
|
|
Question {currentQuestion + 1} of {totalQuestions}
|
|
</span>
|
|
<span className="text-yellow-400 font-semibold">
|
|
{quiz.pointsPerQuestion} points
|
|
</span>
|
|
</div>
|
|
|
|
<div className="w-full bg-gray-700 rounded-full h-2 mb-6 overflow-hidden">
|
|
<div
|
|
className="bg-gradient-to-r from-yellow-500 to-orange-500 h-full rounded-full transition-all duration-500"
|
|
style={{ width: `${progressPercent}%` }}
|
|
></div>
|
|
</div>
|
|
|
|
<QuizQuestion
|
|
question={quiz.questions[currentQuestion]}
|
|
onAnswer={handleAnswer}
|
|
/>
|
|
|
|
{answers[currentQuestion] && (
|
|
<button
|
|
onClick={handleNext}
|
|
className="w-full mt-6 bg-gradient-to-r from-yellow-500 to-orange-500 text-white font-semibold py-3 rounded-lg hover:from-yellow-600 hover:to-orange-600 transition transform hover:scale-105"
|
|
>
|
|
{currentQuestion < totalQuestions - 1 ? "Next Question" : "Complete Quiz"}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</DarkThemeLayout>
|
|
</>
|
|
);
|
|
}
|