Files
starr-philproperties/src/contexts/ClientLibraryContext.jsx
T

204 lines
7.2 KiB
React

// ─── 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 (
<ClientLibraryContext.Provider value={value}>
{children}
</ClientLibraryContext.Provider>
);
}
export default ClientLibraryContext;