// ─── ClientLibraryContext.jsx ─────────────────────────────────────────────── // Standalone Unit / Lesson browsing (junction revamp). Units run independently // of Courses — this context drives the /units grid, /units/:uuid detail page, // and /units/:uuid/read reader. import { createContext, useCallback, useContext, useState } from "react"; import api from "@/utils/api.util"; import { toast } from "sonner"; const ClientLibraryContext = createContext(null); export function useLibrary() { const ctx = useContext(ClientLibraryContext); if (!ctx) throw new Error("useLibrary must be used within a ClientLibraryProvider"); return ctx; } export function ClientLibraryProvider({ children }) { // ── List state const [units, setUnits] = useState([]); const [unitsLoading, setUnitsLoading] = useState(false); const [lessons, setLessons] = useState([]); const [lessonsLoading, setLessonsLoading] = useState(false); // ── Detail state — GET /client/units/:uuid/lessons returns unit + lessons + quiz in one call const [unitDetail, setUnitDetail] = useState(null); const [unitDetailLoading, setUnitDetailLoading] = useState(false); const [unitBlocked, setUnitBlocked] = useState(false); const [unitBlockedInfo, setUnitBlockedInfo] = useState(null); // { message, course } // ── Lesson state (standalone reader) const [lesson, setLesson] = useState(null); const [lessonLoading, setLessonLoading] = useState(false); // ── Quiz state const [quiz, setQuiz] = useState(null); const [quizLoading, setQuizLoading] = useState(false); // ─── Actions ──────────────────────────────────────────────────────────── const getUnits = useCallback(async () => { setUnitsLoading(true); try { const { data } = await api.get("/client/units"); setUnits(data.data ?? []); } catch (err) { toast(err?.response?.data?.message ?? "Could not load units."); } finally { setUnitsLoading(false); } }, []); const getLessons = useCallback(async () => { setLessonsLoading(true); try { const { data } = await api.get("/client/lessons"); setLessons(data.data ?? []); } catch (err) { toast(err?.response?.data?.message ?? "Could not load lessons."); } finally { setLessonsLoading(false); } }, []); const getUnitDetail = useCallback(async (uuid) => { setUnitDetailLoading(true); setUnitBlocked(false); setUnitBlockedInfo(null); try { const { data } = await api.get(`/client/units/${uuid}/lessons`); setUnitDetail(data.data ?? null); } catch (err) { if (err?.response?.status === 403) { setUnitBlocked(true); setUnitBlockedInfo(err.response.data ?? null); } else { toast(err?.response?.data?.message ?? "Could not load unit."); } } finally { setUnitDetailLoading(false); } }, []); const getLesson = useCallback(async (uuid) => { setLessonLoading(true); try { const { data } = await api.get(`/client/lessons/${uuid}`); setLesson(data.data ?? null); } catch (err) { if (err?.response?.status === 403) { setUnitBlocked(true); setUnitBlockedInfo(err.response.data ?? null); } else { toast(err?.response?.data?.message ?? "Could not load lesson."); } } finally { setLessonLoading(false); } }, []); const getUnitQuiz = useCallback(async (uuid) => { setQuizLoading(true); try { const { data } = await api.get(`/client/units/${uuid}/quiz`); setQuiz(data.data ?? null); } catch (err) { toast(err?.response?.data?.message ?? "Could not load quiz."); } finally { setQuizLoading(false); } }, []); const submitUnitQuiz = useCallback(async (uuid, quizId, answers) => { try { const { data } = await api.post(`/client/units/${uuid}/quiz/${quizId}/submit`, { answers }); return data.data ?? null; } catch (err) { toast(err?.response?.data?.message ?? "Could not submit quiz."); return null; } }, []); const saveUnitQuizDraft = useCallback(async (uuid, quizId, answers) => { try { await api.patch(`/client/units/${uuid}/quiz/${quizId}/draft`, { answers }); } catch { /* silent — draft saves are best-effort */ } }, []); // Writes lesson progress, then locally patches unitDetail so the reader/detail // page updates without a full refetch (matched by numeric lesson_id — the // response is keyed by id, not uuid). const upsertLessonProgress = useCallback(async (lessonUuid, status, unitUuid) => { try { const { data } = await api.post(`/client/lessons/${lessonUuid}/progress`, { status, ...(unitUuid ? { unit_uuid: unitUuid } : {}), }); const result = data.data ?? null; // Trust the server's evaluated status for both lesson and unit — no local // re-derivation (previously `lessons.every(status === 'completed')` here, // which drifted from the consolidated evaluator once per-lesson completion // requirements could be configured; the server already ran that same // evaluator via recomputeCascade and returned the authoritative result). setUnitDetail((prev) => { if (!prev || !result?.lesson) return prev; const nextLessons = prev.lessons.map((l) => l.lesson_id === result.lesson.lesson_id ? { ...l, status: result.lesson.status, completed_at: status === "completed" ? new Date().toISOString() : l.completed_at } : l ); const is_completed = result.unit ? result.unit.status === "completed" : prev.is_completed; return { ...prev, lessons: nextLessons, is_completed }; }); return result; } catch (err) { toast(err?.response?.data?.message ?? "Could not update progress."); return null; } }, []); // ─── Resets ───────────────────────────────────────────────────────────── const resetUnitDetail = useCallback(() => { setUnitDetail(null); setUnitBlocked(false); setUnitBlockedInfo(null); }, []); const resetLesson = useCallback(() => setLesson(null), []); const resetQuiz = useCallback(() => setQuiz(null), []); // ─── Value ────────────────────────────────────────────────────────────── const value = { units, unitsLoading, lessons, lessonsLoading, unitDetail, unitDetailLoading, unitBlocked, unitBlockedInfo, lesson, lessonLoading, quiz, quizLoading, getUnits, getLessons, getUnitDetail, getLesson, getUnitQuiz, submitUnitQuiz, saveUnitQuizDraft, upsertLessonProgress, resetUnitDetail, resetLesson, resetQuiz, }; return ( {children} ); } export default ClientLibraryContext;