generated from muhtadeetaron/nextjs-template
feat(zustand): add zustand stores for exam, timer and auth
This commit is contained in:
90
stores/authStore.ts
Normal file
90
stores/authStore.ts
Normal 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
90
stores/examStore.ts
Normal 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
48
stores/timerStore.ts
Normal 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 });
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user