generated from muhtadeetaron/nextjs-template
feat(zustand): add zustand stores for exam, timer and auth
This commit is contained in:
@ -11,10 +11,11 @@ import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { LoginForm } from "@/types/auth";
|
||||
import { CircleAlert } from "lucide-react";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
const LoginPage = () => {
|
||||
const router = useRouter();
|
||||
const { setToken } = useAuth();
|
||||
const { setToken } = useAuthStore();
|
||||
|
||||
const [form, setForm] = useState<LoginForm>({
|
||||
identifier: "",
|
||||
@ -88,7 +89,7 @@ const LoginPage = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{error && <DestructibleAlert text={error} extraStyles="" />}
|
||||
{error && <DestructibleAlert text={error} />}
|
||||
|
||||
<h1 className="flex justify-center items-center gap-2 bg-green-200 p-4 rounded-full">
|
||||
<CircleAlert size={20} />
|
||||
|
||||
@ -11,6 +11,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import FormField from "@/components/FormField";
|
||||
import DestructibleAlert from "@/components/DestructibleAlert";
|
||||
import { RegisterForm } from "@/types/auth";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
interface ValidationErrorItem {
|
||||
type: string;
|
||||
@ -27,7 +28,7 @@ interface CustomError extends Error {
|
||||
}
|
||||
|
||||
export default function RegisterPage() {
|
||||
const { setToken } = useAuth();
|
||||
const { setToken } = useAuthStore();
|
||||
const router = useRouter();
|
||||
const [form, setForm] = useState<RegisterForm>({
|
||||
full_name: "",
|
||||
|
||||
@ -8,6 +8,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import { Loader, RefreshCw } from "lucide-react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
type Mock = {
|
||||
test_id: string;
|
||||
@ -22,7 +23,7 @@ type Mock = {
|
||||
|
||||
export default function MockScreen() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [mocks, setMocks] = useState<Mock[]>([]);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
@ -10,6 +10,7 @@ import { API_URL, getToken } from "@/lib/auth";
|
||||
import { Loader, RefreshCw } from "lucide-react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { Question } from "@/types/exam";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
type Subject = {
|
||||
subject_id: string;
|
||||
@ -19,7 +20,7 @@ type Subject = {
|
||||
|
||||
export default function PaperScreen() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [subjects, setSubjects] = useState<Subject[]>([]);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
@ -70,7 +71,7 @@ export default function PaperScreen() {
|
||||
<Header displayTabTitle="Subjects" />
|
||||
<div className="overflow-y-auto">
|
||||
<div className="mt-5 px-5">
|
||||
<DestructibleAlert text={errorMsg} extraStyles="" />
|
||||
<DestructibleAlert text={errorMsg} />
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<button
|
||||
|
||||
@ -8,6 +8,7 @@ import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { API_URL, getToken } from "@/lib/auth";
|
||||
import { Loader, RefreshCw } from "lucide-react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
|
||||
type Topic = {
|
||||
topic_id: string;
|
||||
@ -21,7 +22,7 @@ type Topic = {
|
||||
|
||||
export default function TopicScreen() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const { user } = useAuthStore();
|
||||
|
||||
const [topics, setTopics] = useState<Topic[]>([]);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
@ -72,7 +73,7 @@ export default function TopicScreen() {
|
||||
<Header displayTabTitle="Subjects" />
|
||||
<div className="overflow-y-auto">
|
||||
<div className="mt-5 px-5">
|
||||
<DestructibleAlert text={errorMsg} extraStyles="" />
|
||||
<DestructibleAlert text={errorMsg} />
|
||||
</div>
|
||||
<div className="flex justify-center mt-4">
|
||||
<button
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { clearAuthToken } from "@/lib/auth";
|
||||
import { useAuthStore } from "@/stores/authStore";
|
||||
import {
|
||||
Bookmark,
|
||||
ChartColumn,
|
||||
@ -23,10 +23,10 @@ import React from "react";
|
||||
|
||||
const SettingsPage = () => {
|
||||
const router = useRouter();
|
||||
const { user, isLoading } = useAuth();
|
||||
const { user, logout } = useAuthStore();
|
||||
|
||||
function handleLogout() {
|
||||
clearAuthToken();
|
||||
logout();
|
||||
router.replace("/");
|
||||
}
|
||||
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useCallback, useMemo } from "react";
|
||||
import React, { useEffect, useCallback } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTimer } from "@/context/TimerContext";
|
||||
import { useExam } from "@/context/ExamContext";
|
||||
import Header from "@/components/Header";
|
||||
import { useModal } from "@/context/ModalContext";
|
||||
import Modal from "@/components/ExamModal";
|
||||
import QuestionItem from "@/components/QuestionItem";
|
||||
import BackgroundWrapper from "@/components/BackgroundWrapper";
|
||||
import { useExamStore } from "@/stores/examStore";
|
||||
import { useTimerStore } from "@/stores/timerStore";
|
||||
|
||||
export default function ExamPage() {
|
||||
const router = useRouter();
|
||||
@ -16,21 +14,25 @@ export default function ExamPage() {
|
||||
const test_id = searchParams.get("test_id") || "";
|
||||
const type = searchParams.get("type") || "";
|
||||
|
||||
// const { isOpen, close, open } = useModal();
|
||||
const { timeRemaining, setInitialTime, stopTimer } = useTimer();
|
||||
const { test, answers, startExam, setAnswer, submitExam, cancelExam } =
|
||||
useExam();
|
||||
useExamStore();
|
||||
const { resetTimer, stopTimer } = useTimerStore();
|
||||
|
||||
// Start exam + timer
|
||||
// Start exam + timer automatically
|
||||
useEffect(() => {
|
||||
if (type && test_id) {
|
||||
startExam(type, test_id).then(() => {
|
||||
if (test?.metadata.time_limit_minutes) {
|
||||
setInitialTime(test.metadata.time_limit_minutes * 60); // convert to seconds
|
||||
startExam(type, test_id).then((fetchedTest) => {
|
||||
if (fetchedTest?.metadata.time_limit_minutes) {
|
||||
resetTimer(fetchedTest.metadata.time_limit_minutes * 60, () => {
|
||||
// Timer ended → auto-submit exam
|
||||
submitExam(type);
|
||||
router.push(`/categories/${type}s`);
|
||||
alert("Time's up! Your exam has been submitted.");
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [type, test_id, startExam, setInitialTime]);
|
||||
}, [type, test_id, startExam, resetTimer, submitExam, router]);
|
||||
|
||||
const showExitDialog = useCallback(() => {
|
||||
if (window.confirm("Are you sure you want to quit the exam?")) {
|
||||
@ -38,7 +40,7 @@ export default function ExamPage() {
|
||||
cancelExam();
|
||||
router.push(`/categories/${type}s`);
|
||||
}
|
||||
}, [stopTimer, cancelExam, router]);
|
||||
}, [stopTimer, cancelExam, router, type]);
|
||||
|
||||
if (!test) {
|
||||
return (
|
||||
@ -51,75 +53,16 @@ export default function ExamPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// answered set for modal overview
|
||||
// const answeredSet = useMemo(
|
||||
// () =>
|
||||
// new Set(
|
||||
// answers
|
||||
// .map((a, idx) =>
|
||||
// a !== null && a !== undefined ? idx.toString() : null
|
||||
// )
|
||||
// .filter(Boolean) as string[]
|
||||
// ),
|
||||
// [answers]
|
||||
// );
|
||||
const handleSubmitExam = (type: string) => {
|
||||
submitExam(type);
|
||||
router.push(`/categories/${type}s`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header with live timer */}
|
||||
<Header />
|
||||
|
||||
{/* Modal: Question overview */}
|
||||
{/* <Modal open={isOpen} onClose={close} title="Exam Overview">
|
||||
<div>
|
||||
<div className="flex gap-6 mb-4">
|
||||
<p className="font-medium">Questions: {test.questions.length}</p>
|
||||
<p className="font-medium">
|
||||
Answered:{" "}
|
||||
<span className="text-blue-900 font-bold">
|
||||
{answeredSet.size}
|
||||
</span>
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
Skipped:{" "}
|
||||
<span className="text-yellow-600 font-bold">
|
||||
{test.questions.length - answeredSet.size}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="h-[0.5px] border-[0.5px] border-black/10 w-full my-3"></div>
|
||||
|
||||
<section className="flex flex-wrap gap-4">
|
||||
{test.questions.map((q, idx) => {
|
||||
const answered = answeredSet.has(String(idx));
|
||||
return (
|
||||
<div
|
||||
key={q.question_id}
|
||||
className={`h-12 w-12 rounded-full flex items-center justify-center cursor-pointer
|
||||
${
|
||||
answered
|
||||
? "bg-blue-900 text-white"
|
||||
: "bg-gray-200 text-gray-900"
|
||||
}
|
||||
hover:opacity-80 transition`}
|
||||
onClick={() => {
|
||||
// optional: scroll to question
|
||||
const el = document.getElementById(`question-${idx}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth" });
|
||||
close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{idx + 1}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
</Modal> */}
|
||||
|
||||
{/* Questions */}
|
||||
<BackgroundWrapper>
|
||||
<div className="container mx-auto px-6 py-8 mb-20">
|
||||
@ -137,7 +80,7 @@ export default function ExamPage() {
|
||||
{/* Bottom submit bar */}
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex">
|
||||
<button
|
||||
onClick={submitExam}
|
||||
onClick={() => handleSubmitExam(type)}
|
||||
className="flex-1 bg-blue-900 text-white p-6 font-bold text-lg hover:bg-blue-800 transition-colors"
|
||||
>
|
||||
Submit
|
||||
|
||||
@ -16,11 +16,12 @@ import { API_URL, getToken } from "@/lib/auth";
|
||||
import { useExam } from "@/context/ExamContext";
|
||||
import { Test } from "@/types/exam";
|
||||
import { Metadata } from "@/types/exam";
|
||||
import { useExamStore } from "@/stores/examStore";
|
||||
|
||||
function PretestPageContent() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { startExam } = useExam();
|
||||
const { startExam } = useExamStore();
|
||||
const [examData, setExamData] = useState<Test>();
|
||||
|
||||
// Get params from URL search params
|
||||
|
||||
@ -3,6 +3,7 @@ import { Montserrat } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
import { Providers } from "./providers";
|
||||
import AuthInitializer from "@/components/AuthInitializer";
|
||||
|
||||
const montserrat = Montserrat({
|
||||
subsets: ["latin"],
|
||||
@ -24,7 +25,9 @@ export default function RootLayout({
|
||||
return (
|
||||
<html lang="en" className={montserrat.variable}>
|
||||
<body className="font-sans">
|
||||
<Providers>{children}</Providers>
|
||||
<AuthInitializer>
|
||||
<Providers>{children}</Providers>
|
||||
</AuthInitializer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@ -6,13 +6,5 @@ import { AuthProvider } from "@/context/AuthContext";
|
||||
import { ModalProvider } from "@/context/ModalContext";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<TimerProvider>
|
||||
<AuthProvider>
|
||||
<ExamProvider>
|
||||
<ModalProvider>{children}</ModalProvider>
|
||||
</ExamProvider>
|
||||
</AuthProvider>
|
||||
</TimerProvider>
|
||||
);
|
||||
return <ModalProvider>{children}</ModalProvider>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user