generated from muhtadeetaron/nextjs-template
feat(ui): add modal functionality, leaderboard view
This commit is contained in:
@ -1,11 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTimer } from "@/context/TimerContext";
|
||||
import { useExam } from "@/context/ExamContext";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import Header from "@/components/Header";
|
||||
import { Bookmark, BookmarkCheck } from "lucide-react";
|
||||
import { useModal } from "@/context/ModalContext";
|
||||
import Modal from "@/components/ExamModal";
|
||||
|
||||
// Types
|
||||
interface Question {
|
||||
@ -21,33 +24,47 @@ interface QuestionItemProps {
|
||||
}
|
||||
|
||||
const QuestionItem = React.memo<QuestionItemProps>(
|
||||
({ question, selectedAnswer, handleSelect }) => (
|
||||
<div className="border border-[#8abdff]/50 rounded-2xl p-4">
|
||||
<h3 className="text-xl font-medium mb-[20px]">
|
||||
{question.id}. {question.question}
|
||||
</h3>
|
||||
<div className="flex flex-col gap-4 items-start">
|
||||
{Object.entries(question.options).map(([key, value]) => (
|
||||
<button
|
||||
key={key}
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => handleSelect(question.id, key)}
|
||||
>
|
||||
<span
|
||||
className={`flex items-center rounded-full border px-1.5 ${
|
||||
selectedAnswer === key
|
||||
? "text-white bg-[#113768] border-[#113768]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{key.toUpperCase()}
|
||||
</span>
|
||||
<span className="option-description">{value}</span>
|
||||
({ question, selectedAnswer, handleSelect }) => {
|
||||
const [bookmark, setBookmark] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="border-[0.5px] border-[#8abdff]/60 rounded-2xl p-4 flex flex-col">
|
||||
<h3 className="text-xl font-medium mb-[20px]">
|
||||
{question.id}. {question.question}
|
||||
</h3>
|
||||
<div className="flex justify-between items-center">
|
||||
<div></div>
|
||||
<button onClick={() => setBookmark(!bookmark)}>
|
||||
{bookmark ? (
|
||||
<BookmarkCheck size={25} color="#113768" />
|
||||
) : (
|
||||
<Bookmark size={25} color="#113768" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 items-start">
|
||||
{Object.entries(question.options).map(([key, value]) => (
|
||||
<button
|
||||
key={key}
|
||||
className="flex items-center gap-3"
|
||||
onClick={() => handleSelect(question.id, key)}
|
||||
>
|
||||
<span
|
||||
className={`flex items-center rounded-full border px-1.5 ${
|
||||
selectedAnswer === key
|
||||
? "text-white bg-[#113768] border-[#113768]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{key.toUpperCase()}
|
||||
</span>
|
||||
<span className="option-description">{value}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
QuestionItem.displayName = "QuestionItem";
|
||||
@ -56,6 +73,7 @@ export default function ExamPage() {
|
||||
const router = useRouter();
|
||||
const { id } = useParams();
|
||||
const time = useSearchParams().get("time");
|
||||
const { isOpen, close } = useModal();
|
||||
|
||||
const { setInitialTime, stopTimer } = useTimer();
|
||||
const {
|
||||
@ -76,17 +94,6 @@ export default function ExamPage() {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submissionLoading, setSubmissionLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
"hydrated:",
|
||||
isHydrated,
|
||||
"initialized:",
|
||||
isInitialized,
|
||||
"exam:",
|
||||
currentExam
|
||||
);
|
||||
}, [isHydrated, isInitialized, currentExam]);
|
||||
|
||||
// Initial checks
|
||||
useEffect(() => {
|
||||
if (!isHydrated || !isInitialized || isSubmitting) return;
|
||||
@ -203,6 +210,11 @@ export default function ExamPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const answeredSet = useMemo(() => {
|
||||
if (!currentAttempt) return new Set<string>();
|
||||
return new Set(currentAttempt.answers.map((a) => String(a.questionId)));
|
||||
}, [currentAttempt]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<Header
|
||||
@ -212,7 +224,54 @@ export default function ExamPage() {
|
||||
displayUser={undefined}
|
||||
displaySubject={undefined}
|
||||
/>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<Modal open={isOpen} onClose={close} title={currentExam?.title}>
|
||||
{currentAttempt ? (
|
||||
<div>
|
||||
<div className="flex gap-4">
|
||||
<p className="">Questions: {currentExam?.questions.length}</p>
|
||||
<p className="">
|
||||
Answers:{" "}
|
||||
<span className="text-[#113768] font-bold">
|
||||
{currentAttempt?.answers.length}
|
||||
</span>
|
||||
</p>
|
||||
<p className="">
|
||||
Skipped:{" "}
|
||||
<span className="text-yellow-600 font-bold">
|
||||
{currentExam?.questions.length -
|
||||
currentAttempt?.answers.length}
|
||||
</span>
|
||||
</p>
|
||||
|
||||
{/* more details */}
|
||||
</div>
|
||||
<div className="h-[0.5px] border-[0.5px] border-black/10 w-full my-3"></div>
|
||||
<section className="flex flex-wrap gap-4">
|
||||
{currentExam?.questions.map((q, idx) => {
|
||||
const answered = answeredSet.has(String(q.id)); // ← convert to string
|
||||
|
||||
return (
|
||||
<div
|
||||
key={q.id ?? idx}
|
||||
className={`h-16 w-16 rounded-full flex items-center justify-center
|
||||
text-2xl
|
||||
${
|
||||
answered
|
||||
? "bg-[#0E2C53] text-white font-semibold"
|
||||
: "bg-[#E9EDF1] text-black font-normal"
|
||||
}`}
|
||||
>
|
||||
{idx + 1}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
) : (
|
||||
<p>No attempt data.</p>
|
||||
)}
|
||||
</Modal>
|
||||
<div className="container mx-auto px-6 py-8">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center min-h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-900"></div>
|
||||
@ -231,15 +290,13 @@ export default function ExamPage() {
|
||||
)}
|
||||
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-4">
|
||||
<div className="container mx-auto">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submissionLoading}
|
||||
className="w-full bg-blue-900 text-white py-4 px-6 rounded-lg font-bold text-lg hover:bg-blue-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submissionLoading}
|
||||
className="w-full bg-blue-900 text-white py-4 px-6 rounded-lg font-bold text-lg hover:bg-blue-800 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user