generated from muhtadeetaron/nextjs-template
feat(ui): add modal functionality, leaderboard view
This commit is contained in:
@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||||
|
import Header from "@/components/Header";
|
||||||
|
import { API_URL, getToken } from "@/lib/auth";
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const page = () => {
|
||||||
|
const [boardError, setBoardError] = useState<string | null>(null);
|
||||||
|
const [boardData, setBoardData] = useState([]);
|
||||||
|
const [userData, setUserData] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
async function fetchBoardData() {
|
||||||
|
try {
|
||||||
|
const boardResponse = await fetch(`${API_URL}/leaderboard`, {
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!boardResponse.ok) {
|
||||||
|
throw new Error("Failed to fetch leaderboard data");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedBoardData = await boardResponse.json();
|
||||||
|
if (Array.isArray(fetchedBoardData) && fetchedBoardData.length > 0) {
|
||||||
|
setBoardData(fetchedBoardData);
|
||||||
|
} else {
|
||||||
|
setBoardError("No leaderboard data available.");
|
||||||
|
setBoardData([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setBoardError("Something went wrong. Please try again.");
|
||||||
|
setBoardData([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchUser() {
|
||||||
|
try {
|
||||||
|
const token = await getToken();
|
||||||
|
if (!token) throw new Error("User is not authenticated");
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}/me`, {
|
||||||
|
method: "get",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch user data");
|
||||||
|
|
||||||
|
const fetchedUserData = await response.json();
|
||||||
|
setLoading(false);
|
||||||
|
setUserData(fetchedUserData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
setUserData(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchUser();
|
||||||
|
fetchBoardData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getTopThree = (boardData) => {
|
||||||
|
if (!boardData || !Array.isArray(boardData)) return [];
|
||||||
|
const sortedData = boardData
|
||||||
|
.filter((player) => player?.points !== undefined) // Ensure `points` exists
|
||||||
|
.sort((a, b) => b.points - a.points);
|
||||||
|
|
||||||
|
const topThree = sortedData.slice(0, 3).map((player, index) => ({
|
||||||
|
...player,
|
||||||
|
rank: index + 1,
|
||||||
|
height: index === 0 ? 280 : index === 1 ? 250 : 220,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [topThree[1], topThree[0], topThree[2]].filter(Boolean); // Handle missing players
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLeaderboard = (boardData) => {
|
||||||
|
return boardData.slice().sort((a, b) => b.points - a.points);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserData = (boardData, name) => {
|
||||||
|
if (!boardData || !Array.isArray(boardData)) return [];
|
||||||
|
const sortedData = boardData
|
||||||
|
.filter((player) => player?.name && player?.points !== undefined)
|
||||||
|
.sort((a, b) => b.points - a.points);
|
||||||
|
|
||||||
|
const result = sortedData.find((player) => player.name === name);
|
||||||
|
return result ? [{ ...result, rank: sortedData.indexOf(result) + 1 }] : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackgroundWrapper>
|
||||||
|
<section>
|
||||||
|
<Header displaySubject={"Leaderboard"} displayTabTitle={null} />
|
||||||
|
</section>
|
||||||
|
</BackgroundWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default page;
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
"use client";
|
"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 { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useTimer } from "@/context/TimerContext";
|
import { useTimer } from "@/context/TimerContext";
|
||||||
import { useExam } from "@/context/ExamContext";
|
import { useExam } from "@/context/ExamContext";
|
||||||
import { API_URL, getToken } from "@/lib/auth";
|
import { API_URL, getToken } from "@/lib/auth";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
|
import { Bookmark, BookmarkCheck } from "lucide-react";
|
||||||
|
import { useModal } from "@/context/ModalContext";
|
||||||
|
import Modal from "@/components/ExamModal";
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
interface Question {
|
interface Question {
|
||||||
@ -21,33 +24,47 @@ interface QuestionItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const QuestionItem = React.memo<QuestionItemProps>(
|
const QuestionItem = React.memo<QuestionItemProps>(
|
||||||
({ question, selectedAnswer, handleSelect }) => (
|
({ question, selectedAnswer, handleSelect }) => {
|
||||||
<div className="border border-[#8abdff]/50 rounded-2xl p-4">
|
const [bookmark, setBookmark] = useState(false);
|
||||||
<h3 className="text-xl font-medium mb-[20px]">
|
|
||||||
{question.id}. {question.question}
|
return (
|
||||||
</h3>
|
<div className="border-[0.5px] border-[#8abdff]/60 rounded-2xl p-4 flex flex-col">
|
||||||
<div className="flex flex-col gap-4 items-start">
|
<h3 className="text-xl font-medium mb-[20px]">
|
||||||
{Object.entries(question.options).map(([key, value]) => (
|
{question.id}. {question.question}
|
||||||
<button
|
</h3>
|
||||||
key={key}
|
<div className="flex justify-between items-center">
|
||||||
className="flex items-center gap-3"
|
<div></div>
|
||||||
onClick={() => handleSelect(question.id, key)}
|
<button onClick={() => setBookmark(!bookmark)}>
|
||||||
>
|
{bookmark ? (
|
||||||
<span
|
<BookmarkCheck size={25} color="#113768" />
|
||||||
className={`flex items-center rounded-full border px-1.5 ${
|
) : (
|
||||||
selectedAnswer === key
|
<Bookmark size={25} color="#113768" />
|
||||||
? "text-white bg-[#113768] border-[#113768]"
|
)}
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{key.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
<span className="option-description">{value}</span>
|
|
||||||
</button>
|
</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>
|
||||||
</div>
|
);
|
||||||
)
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
QuestionItem.displayName = "QuestionItem";
|
QuestionItem.displayName = "QuestionItem";
|
||||||
@ -56,6 +73,7 @@ export default function ExamPage() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const time = useSearchParams().get("time");
|
const time = useSearchParams().get("time");
|
||||||
|
const { isOpen, close } = useModal();
|
||||||
|
|
||||||
const { setInitialTime, stopTimer } = useTimer();
|
const { setInitialTime, stopTimer } = useTimer();
|
||||||
const {
|
const {
|
||||||
@ -76,17 +94,6 @@ export default function ExamPage() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [submissionLoading, setSubmissionLoading] = useState(false);
|
const [submissionLoading, setSubmissionLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(
|
|
||||||
"hydrated:",
|
|
||||||
isHydrated,
|
|
||||||
"initialized:",
|
|
||||||
isInitialized,
|
|
||||||
"exam:",
|
|
||||||
currentExam
|
|
||||||
);
|
|
||||||
}, [isHydrated, isInitialized, currentExam]);
|
|
||||||
|
|
||||||
// Initial checks
|
// Initial checks
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isHydrated || !isInitialized || isSubmitting) return;
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<Header
|
<Header
|
||||||
@ -212,7 +224,54 @@ export default function ExamPage() {
|
|||||||
displayUser={undefined}
|
displayUser={undefined}
|
||||||
displaySubject={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 ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center min-h-64">
|
<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>
|
<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="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 p-4">
|
||||||
<div className="container mx-auto">
|
<button
|
||||||
<button
|
onClick={handleSubmit}
|
||||||
onClick={handleSubmit}
|
disabled={submissionLoading}
|
||||||
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"
|
||||||
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
|
||||||
Submit
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -31,18 +31,18 @@ const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => (
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
||||||
{selectedAnswer?.answer === question.correctAnswer ? (
|
{!selectedAnswer ? (
|
||||||
|
<Badge className="bg-yellow-500" variant="destructive">
|
||||||
|
Skipped
|
||||||
|
</Badge>
|
||||||
|
) : selectedAnswer.answer === question.correctAnswer ? (
|
||||||
<Badge className="bg-green-500 text-white" variant="default">
|
<Badge className="bg-green-500 text-white" variant="default">
|
||||||
Correct
|
Correct
|
||||||
</Badge>
|
</Badge>
|
||||||
) : selectedAnswer?.answer !== question.correctAnswer ? (
|
) : (
|
||||||
<Badge className="bg-red-500 text-white" variant="default">
|
<Badge className="bg-red-500 text-white" variant="default">
|
||||||
Incorrect
|
Incorrect
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
|
||||||
<Badge className="bg-yellow-500" variant="destructive">
|
|
||||||
Skipped
|
|
||||||
</Badge>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ const QuestionItem = ({ question, selectedAnswer }: QuestionItemProps) => (
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="h-[0.5px] border-[0.5px] border-dashed border-black/20"></div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h3 className="text-lg font-bold text-black/40">Solution:</h3>
|
<h3 className="text-lg font-bold text-black/40">Solution:</h3>
|
||||||
<p className="text-lg">{question.solution}</p>
|
<p className="text-lg">{question.solution}</p>
|
||||||
@ -96,6 +96,7 @@ export default function ResultsPage() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
examResults = useExamResults();
|
examResults = useExamResults();
|
||||||
|
console.log(examResults);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle case where there's no completed exam
|
// Handle case where there's no completed exam
|
||||||
return (
|
return (
|
||||||
@ -221,12 +222,16 @@ export default function ResultsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-white">
|
||||||
<button className="p-10" onClick={() => router.push("/unit")}>
|
<button className="p-10" onClick={() => handleBackToHome()}>
|
||||||
<ArrowLeft size={30} color="black" />
|
<ArrowLeft size={30} color="black" />
|
||||||
</button>
|
</button>
|
||||||
<div className="bg-white rounded-lg shadow-lg px-10 pb-20">
|
<div className="bg-white rounded-lg shadow-lg px-10 pb-20">
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mb-4 text-center">
|
<h1 className="text-2xl font-bold text-[#113768] mb-4 text-center">
|
||||||
Keep up the good work!
|
{examResults?.score < 30
|
||||||
|
? "Try harder!"
|
||||||
|
: examResults?.score < 70
|
||||||
|
? "Getting Better"
|
||||||
|
: "You did great!"}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{/* Score Display */}
|
{/* Score Display */}
|
||||||
@ -246,16 +251,6 @@ export default function ResultsPage() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4 items-center mb-6 text-sm text-gray-600">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 bg-green-600 rounded-full"></div>{" "}
|
|
||||||
Correct
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 bg-red-600 rounded-full"></div> Your
|
|
||||||
Answer (Incorrect)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -3,12 +3,15 @@
|
|||||||
import { ExamProvider } from "@/context/ExamContext";
|
import { ExamProvider } from "@/context/ExamContext";
|
||||||
import { TimerProvider } from "@/context/TimerContext";
|
import { TimerProvider } from "@/context/TimerContext";
|
||||||
import { AuthProvider } from "@/context/AuthContext";
|
import { AuthProvider } from "@/context/AuthContext";
|
||||||
|
import { ModalProvider } from "@/context/ModalContext";
|
||||||
|
|
||||||
export function Providers({ children }: { children: React.ReactNode }) {
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<TimerProvider>
|
<TimerProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<ExamProvider>{children}</ExamProvider>
|
<ExamProvider>
|
||||||
|
<ModalProvider>{children}</ModalProvider>
|
||||||
|
</ExamProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</TimerProvider>
|
</TimerProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
91
components/ExamModal.tsx
Normal file
91
components/ExamModal.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
interface ModalProps {
|
||||||
|
/** Control visibility */
|
||||||
|
open: boolean;
|
||||||
|
/** Callback for both explicit and implicit close actions */
|
||||||
|
onClose: () => void;
|
||||||
|
/** Optional heading */
|
||||||
|
title?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
/** Center horizontally and vertically? (default true) */
|
||||||
|
center?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Modal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
center = true,
|
||||||
|
}: ModalProps) {
|
||||||
|
const dialogRef = useRef<HTMLDialogElement | null>(null);
|
||||||
|
|
||||||
|
// Open / close imperatively to keep <dialog> in sync with prop
|
||||||
|
useEffect(() => {
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
if (open && !dialog.open) dialog.showModal();
|
||||||
|
if (!open && dialog.open) dialog.close();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// Close on native <dialog> close event
|
||||||
|
useEffect(() => {
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
const handleClose = () => onClose();
|
||||||
|
dialog.addEventListener("close", handleClose);
|
||||||
|
return () => dialog.removeEventListener("close", handleClose);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
// ESC -> close (for browsers without built‑in <dialog> handling)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
|
}, [open, onClose]);
|
||||||
|
|
||||||
|
if (typeof window === "undefined") return null; // SSR guard
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<dialog
|
||||||
|
ref={dialogRef}
|
||||||
|
className={`fixed top-0 right-0 h-[110vh] z-50 w-[87vw]
|
||||||
|
ml-auto transform-none /* 1 & 2 */
|
||||||
|
backdrop:bg-black/20
|
||||||
|
transition-[opacity,transform] duration-300
|
||||||
|
${
|
||||||
|
open
|
||||||
|
? "opacity-100 translate-x-0 translate-y-0"
|
||||||
|
: "opacity-0 translate-x-4"
|
||||||
|
}
|
||||||
|
${center ? "rounded-l-xl" : ""}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Card */}
|
||||||
|
<div className="bg-white rounded-xl overflow-hidden pb-10">
|
||||||
|
{title && (
|
||||||
|
<header className="flex items-center justify-between px-6 pt-10 pb-4 dark:border-zinc-700">
|
||||||
|
<h2 className="text-2xl font-semibold">{title}</h2>
|
||||||
|
<button
|
||||||
|
aria-label="Close"
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section className="px-6 ">{children}</section>
|
||||||
|
</div>
|
||||||
|
</dialog>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import { useTimer } from "@/context/TimerContext";
|
|||||||
import styles from "@/css/Header.module.css";
|
import styles from "@/css/Header.module.css";
|
||||||
import { useExam } from "@/context/ExamContext";
|
import { useExam } from "@/context/ExamContext";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { useModal } from "@/context/ModalContext";
|
||||||
|
|
||||||
const API_URL = "https://examjam-api.pptx704.com";
|
const API_URL = "https://examjam-api.pptx704.com";
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ const Header = ({
|
|||||||
examDuration,
|
examDuration,
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { open } = useModal();
|
||||||
const { clearExam } = useExam();
|
const { clearExam } = useExam();
|
||||||
const [totalSeconds, setTotalSeconds] = useState(
|
const [totalSeconds, setTotalSeconds] = useState(
|
||||||
examDuration ? parseInt(examDuration) * 60 : 0
|
examDuration ? parseInt(examDuration) * 60 : 0
|
||||||
@ -147,7 +149,7 @@ const Header = ({
|
|||||||
</span>
|
</span>
|
||||||
<span className={styles.timeLabel}>Mins</span>
|
<span className={styles.timeLabel}>Mins</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.timeUnit}>
|
<div className={styles.timeUnit} style={{ borderRight: "none" }}>
|
||||||
<span className={styles.timeValue}>
|
<span className={styles.timeValue}>
|
||||||
{String(seconds).padStart(2, "0")}
|
{String(seconds).padStart(2, "0")}
|
||||||
</span>
|
</span>
|
||||||
@ -156,8 +158,7 @@ const Header = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
disabled
|
onClick={open}
|
||||||
onClick={() => router.push("/exam/modal")}
|
|
||||||
className={`${styles.iconButton} ${styles.disabled}`}
|
className={`${styles.iconButton} ${styles.disabled}`}
|
||||||
>
|
>
|
||||||
<Layers size={30} color="white" />
|
<Layers size={30} color="white" />
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useEffect } from "react";
|
import React, { createContext, useContext, useState, useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, usePathname } from "next/navigation";
|
||||||
|
|
||||||
interface AuthContextType {
|
interface AuthContextType {
|
||||||
token: string | null;
|
token: string | null;
|
||||||
@ -47,6 +47,7 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
const [token, setTokenState] = useState<string | null>(null);
|
const [token, setTokenState] = useState<string | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
// Custom setToken function that also updates cookies
|
// Custom setToken function that also updates cookies
|
||||||
const setToken = (newToken: string | null) => {
|
const setToken = (newToken: string | null) => {
|
||||||
@ -58,21 +59,22 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeAuth = () => {
|
const initializeAuth = () => {
|
||||||
const storedToken = getCookie("authToken");
|
const storedToken = getCookie("authToken");
|
||||||
|
console.log("Current pathname:", pathname);
|
||||||
|
console.log("Stored token:", storedToken);
|
||||||
|
|
||||||
if (storedToken) {
|
if (storedToken) {
|
||||||
setTokenState(storedToken);
|
setTokenState(storedToken);
|
||||||
// Only redirect if we're on login/register pages
|
|
||||||
if (
|
if (
|
||||||
router.pathname === "/" ||
|
pathname === "/" ||
|
||||||
router.pathname === "/login" ||
|
pathname === "/login" ||
|
||||||
router.pathname === "/register"
|
pathname === "/register"
|
||||||
) {
|
) {
|
||||||
|
console.log("Redirecting to /home");
|
||||||
router.replace("/home");
|
router.replace("/home");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Only redirect to login if we're on a protected page
|
|
||||||
const publicPages = ["/", "/login", "/register"];
|
const publicPages = ["/", "/login", "/register"];
|
||||||
if (!publicPages.includes(router.pathname)) {
|
if (!publicPages.includes(pathname)) {
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -80,10 +82,8 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Small delay to ensure router is ready
|
initializeAuth();
|
||||||
const timer = setTimeout(initializeAuth, 100);
|
}, [pathname, router]);
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [router.pathname]);
|
|
||||||
|
|
||||||
// Function to log out
|
// Function to log out
|
||||||
const logout = () => {
|
const logout = () => {
|
||||||
|
|||||||
24
context/ModalContext.tsx
Normal file
24
context/ModalContext.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
|
const ModalContext = createContext(null);
|
||||||
|
|
||||||
|
export function ModalProvider({ children }) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const open = () => setIsOpen(true);
|
||||||
|
const close = () => setIsOpen(false);
|
||||||
|
const toggle = () => setIsOpen((prev) => !prev);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={{ isOpen, open, close, toggle }}>
|
||||||
|
{children}
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useModal() {
|
||||||
|
const ctx = useContext(ModalContext);
|
||||||
|
if (!ctx) throw new Error("useModal must be inside <ModalProvider>");
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
@ -79,6 +79,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-right: 1px solid #000;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeValue {
|
.timeValue {
|
||||||
@ -153,7 +155,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timer {
|
.timer {
|
||||||
width: 160px;
|
width: 180px;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user