mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
204 lines
7.2 KiB
React
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;
|