feat(zustand): add zustand stores for exam, timer and auth

This commit is contained in:
shafin-r
2025-08-31 18:28:01 +06:00
parent 65e3338859
commit 7df2708db7
18 changed files with 352 additions and 106 deletions

90
stores/authStore.ts Normal file
View File

@ -0,0 +1,90 @@
"use client";
import { create } from "zustand";
import { UserData } from "@/types/auth";
import { API_URL } from "@/lib/auth";
// Cookie utilities
const getCookie = (name: string): string | null => {
if (typeof document === "undefined") return null;
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop()?.split(";").shift() || null;
}
return null;
};
const setCookie = (
name: string,
value: string | null,
days: number = 7
): void => {
if (typeof document === "undefined") return;
if (value === null) {
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/; SameSite=Strict; Secure`;
} else {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
document.cookie = `${name}=${value}; expires=${expires.toUTCString()}; path=/; SameSite=Strict; Secure`;
}
};
interface AuthState {
token: string | null;
isLoading: boolean;
user: UserData | null;
setToken: (token: string | null) => void;
fetchUser: () => Promise<void>;
logout: () => void;
initializeAuth: () => Promise<void>;
}
export const useAuthStore = create<AuthState>((set, get) => ({
token: null,
isLoading: true,
user: null,
setToken: (newToken) => {
set({ token: newToken });
setCookie("authToken", newToken);
},
fetchUser: async () => {
const token = get().token;
if (!token) return;
try {
const res = await fetch(`${API_URL}/me/profile/`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) throw new Error("Failed to fetch user info");
const data: UserData = await res.json();
set({ user: data });
} catch (err) {
console.error("Error fetching user:", err);
get().logout();
}
},
logout: () => {
set({ token: null, user: null });
setCookie("authToken", null);
},
initializeAuth: async () => {
const storedToken = getCookie("authToken");
if (storedToken) {
set({ token: storedToken });
await get().fetchUser();
}
set({ isLoading: false });
},
}));

90
stores/examStore.ts Normal file
View File

@ -0,0 +1,90 @@
"use client";
import { create } from "zustand";
import { Test, Answer } from "@/types/exam";
import { API_URL, getToken } from "@/lib/auth";
interface ExamState {
test: Test | null;
answers: Answer[];
startExam: (testType: string, testId: string) => Promise<void>;
setAnswer: (questionIndex: number, answer: Answer) => void;
submitExam: (testType: string) => Promise<void>;
cancelExam: () => void;
}
export const useExamStore = create<ExamState>((set, get) => ({
test: null,
answers: [],
// start exam
startExam: async (testType: string, testId: string) => {
try {
const token = await getToken();
const res = await fetch(`${API_URL}/tests/${testType}/${testId}`, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) throw new Error(`Failed to fetch test: ${res.status}`);
const data: Test = await res.json();
set({
test: data,
answers: Array(data.questions.length).fill(null),
});
} catch (err) {
console.error("startExam error:", err);
}
},
// set an answer
setAnswer: (questionIndex: number, answer: Answer) => {
set((state) => {
const updated = [...state.answers];
updated[questionIndex] = answer;
return { answers: updated };
});
},
// submit exam
submitExam: async (testType: string) => {
const { test, answers } = get();
if (!test) return;
const token = await getToken();
try {
const { test_id, attempt_id } = test.metadata;
console.log(answers);
const res = await fetch(
`${API_URL}/tests/${testType}/${test_id}/${attempt_id}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ answers }),
}
);
console.log(res);
if (!res.ok) throw new Error("Failed to submit exam");
// reset store
set({ test: null, answers: [] });
} catch (err) {
console.error("Failed to submit exam. Reason:", err);
}
},
// cancel exam
cancelExam: () => {
set({ test: null, answers: [] });
},
}));

48
stores/timerStore.ts Normal file
View File

@ -0,0 +1,48 @@
"use client";
import { create } from "zustand";
interface TimerState {
timeRemaining: number;
timerRef: NodeJS.Timeout | null;
resetTimer: (duration: number, onComplete?: () => void) => void;
stopTimer: () => void;
setInitialTime: (duration: number) => void;
}
export const useTimerStore = create<TimerState>((set, get) => ({
timeRemaining: 0,
timerRef: null,
resetTimer: (duration, onComplete) => {
const { timerRef } = get();
if (timerRef) clearInterval(timerRef);
const newRef = setInterval(() => {
set((state) => {
if (state.timeRemaining <= 1) {
clearInterval(newRef);
if (onComplete) onComplete(); // ✅ call callback when timer ends
return { timeRemaining: 0, timerRef: null };
}
return { timeRemaining: state.timeRemaining - 1 };
});
}, 1000);
set({ timeRemaining: duration, timerRef: newRef });
},
stopTimer: () => {
const { timerRef } = get();
if (timerRef) clearInterval(timerRef);
set({ timeRemaining: 0, timerRef: null });
},
setInitialTime: (duration) => {
const { timerRef } = get();
if (timerRef) clearInterval(timerRef);
set({ timeRemaining: duration, timerRef: null });
},
}));