testing 127.0.0.1 issue

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-07 13:40:07 +08:00
parent 244aa607f7
commit 6624c0d27f
62 changed files with 662 additions and 1889 deletions
@@ -82,7 +82,8 @@ const AppBreadcrumb = ({ items = [], color = {} }) => {
{isLast ? (
<BreadcrumbPage className={cn("flex items-center gap-2 max-w-[300px] truncate", pageColor)}>
{item.icon}
<span className="truncate">{item.label}</span>
<span className="hidden lg:inline truncate">{item.label}</span>
<span className="lg:hidden">...</span>
</BreadcrumbPage>
) : (
<BreadcrumbLink asChild>
-2
View File
@@ -186,7 +186,6 @@ function SignOutOverlay({ open }) {
// ─── Main UserMenu ────────────────────────────────────────────────────────────
export default function UserMenu() {
const { user, logout } = useAuth()
const { setTheme } = useTheme()
const { avatarUrl } = useProfile()
const navigate = useNavigate()
@@ -208,7 +207,6 @@ export default function UserMenu() {
const handleLogout = async () => {
setSigningOut(true)
await logout()
setTheme('light')
navigate('/login', { replace: true })
}
+5 -1
View File
@@ -11,6 +11,7 @@ const Toaster = ({
<Sonner
theme={theme}
className="toaster group"
closeButton
icons={{
success: (
<CircleCheckIcon className="size-4" />
@@ -33,7 +34,10 @@ const Toaster = ({
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)"
"--border-radius": "var(--radius)",
"--toast-close-button-start": "unset",
"--toast-close-button-end": "0",
"--toast-close-button-transform": "translate(35%, -35%)"
}
}
toastOptions={{
+4 -24
View File
@@ -19,12 +19,7 @@ export function AdminAchievementsProvider({ children }) {
setLoading(true);
try { return await fn(); }
catch (err) {
toast(err?.response?.data?.message ?? "Something went wrong.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Something went wrong.");
return null;
} finally { setLoading(false); }
}, []);
@@ -46,12 +41,7 @@ export function AdminAchievementsProvider({ children }) {
const createAchievement = useCallback((payload) =>
request(async () => {
const { data } = await api.post("/admin/achievements", payload);
toast("Achievement created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Achievement created.");
return data.data;
}), [request]);
@@ -62,12 +52,7 @@ export function AdminAchievementsProvider({ children }) {
prev.map((a) => (String(a.achievement_definition_id) === String(id) ? data.data : a))
);
if (achievement && String(achievement.achievement_definition_id) === String(id)) setAchievement(data.data);
toast("Achievement updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Achievement updated.");
return data.data;
}), [request, achievement]);
@@ -75,12 +60,7 @@ export function AdminAchievementsProvider({ children }) {
request(async () => {
await api.delete(`/admin/achievements/${id}`);
setAchievements((prev) => prev.filter((a) => String(a.achievement_definition_id) !== String(id)));
toast("Achievement deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Achievement deleted.");
return true;
}), [request]);
+9 -54
View File
@@ -34,12 +34,7 @@ export function AdvertisementsProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? "Something went wrong.";
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -105,12 +100,7 @@ export function AdvertisementsProvider({ children }) {
const advertisement = res.data?.data?.data ?? null;
if (advertisement) {
setAdvertisements((prev) => [advertisement, ...prev]);
toast("Advertisement created successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Advertisement created successfully.");
}
return res.data;
}),
@@ -126,12 +116,7 @@ export function AdvertisementsProvider({ children }) {
if (advertisement) {
setAdvertisements((prev) => prev.map((a) => (a.advertisement_id === advertisementId ? advertisement : a)));
setSelectedAdvertisement(advertisement);
toast("Advertisement updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Advertisement updated successfully.");
}
return res.data;
}),
@@ -147,12 +132,7 @@ export function AdvertisementsProvider({ children }) {
});
setAdvertisements((prev) => prev.filter((a) => a.advertisement_id !== advertisementId));
setSelectedAdvertisement((prev) => (prev?.advertisement_id === advertisementId ? null : prev));
toast("Advertisement archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Advertisement archived.");
return res.data;
}),
[request]
@@ -166,12 +146,7 @@ export function AdvertisementsProvider({ children }) {
data: { ids, deletedBy },
});
setAdvertisements((prev) => prev.filter((a) => !ids.includes(a.advertisement_id)));
toast(`${ids.length} advertisement(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} advertisement(s) archived.`);
return res.data;
}),
[request]
@@ -185,12 +160,7 @@ export function AdvertisementsProvider({ children }) {
const advertisement = res.data?.data?.data ?? null;
if (advertisement) {
setAdvertisements((prev) => prev.filter((a) => a.advertisement_id !== advertisementId));
toast("Advertisement restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Advertisement restored.");
}
return res.data;
}),
@@ -203,12 +173,7 @@ export function AdvertisementsProvider({ children }) {
request(async () => {
const res = await api.patch("/admin/advertisements/bulk-restore", { ids });
setAdvertisements((prev) => prev.filter((a) => !ids.includes(a.advertisement_id)));
toast(`${ids.length} advertisement(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} advertisement(s) restored.`);
return res.data;
}),
[request]
@@ -220,12 +185,7 @@ export function AdvertisementsProvider({ children }) {
request(async () => {
const res = await api.delete(`/admin/advertisements/${advertisementId}/permanent`);
setAdvertisements((prev) => prev.filter((a) => a.advertisement_id !== advertisementId));
toast("Advertisement permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Advertisement permanently deleted.");
return res.data;
}),
[request]
@@ -237,12 +197,7 @@ export function AdvertisementsProvider({ children }) {
request(async () => {
const res = await api.delete("/admin/advertisements/bulk/permanent", { data: { ids } });
setAdvertisements((prev) => prev.filter((a) => !ids.includes(a.advertisement_id)));
toast(`${ids.length} advertisement(s) permanently deleted.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} advertisement(s) permanently deleted.`);
return res.data;
}),
[request]
+9 -54
View File
@@ -69,12 +69,7 @@ export function AssetsProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? "Something went wrong.";
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -206,12 +201,7 @@ export function AssetsProvider({ children }) {
if (asset) {
setAssets((prev) => [asset, ...prev]);
invalidateListCache();
toast("Asset uploaded successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Asset uploaded successfully.");
}
return res.data;
}),
@@ -237,12 +227,7 @@ export function AssetsProvider({ children }) {
setAssets((prev) => prev.map((a) => (a.asset_id === assetId ? asset : a)));
setSelectedAsset(asset);
invalidateListCache();
toast("Asset updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Asset updated successfully.");
}
return res.data;
}),
@@ -259,12 +244,7 @@ export function AssetsProvider({ children }) {
setAssets((prev) => prev.filter((a) => a.asset_id !== assetId));
setSelectedAsset((prev) => (prev?.asset_id === assetId ? null : prev));
invalidateListCache();
toast("Asset archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Asset archived.");
return res.data;
}),
[request]
@@ -279,12 +259,7 @@ export function AssetsProvider({ children }) {
});
setAssets((prev) => prev.filter((a) => !ids.includes(a.asset_id)));
invalidateListCache();
toast(`${ids.length} asset(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} asset(s) archived.`);
return res.data;
}),
[request]
@@ -299,12 +274,7 @@ export function AssetsProvider({ children }) {
if (asset) {
setAssets((prev) => prev.filter((a) => a.asset_id !== assetId));
invalidateListCache();
toast("Asset restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Asset restored.");
}
return res.data;
}),
@@ -318,12 +288,7 @@ export function AssetsProvider({ children }) {
const res = await api.patch("/admin/assets/bulk-restore", { ids });
setAssets((prev) => prev.filter((a) => !ids.includes(a.asset_id)));
invalidateListCache();
toast(`${ids.length} asset(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} asset(s) restored.`);
return res.data;
}),
[request]
@@ -337,12 +302,7 @@ export function AssetsProvider({ children }) {
setAssets((prev) => prev.filter((a) => a.asset_id !== assetId));
setSelectedAsset((prev) => (prev?.asset_id === assetId ? null : prev));
invalidateListCache();
toast("Asset permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Asset permanently deleted.");
return res.data;
}),
[request]
@@ -357,12 +317,7 @@ export function AssetsProvider({ children }) {
});
setAssets((prev) => prev.filter((a) => !ids.includes(a.asset_id)));
invalidateListCache();
toast(`${ids.length} asset(s) permanently deleted.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} asset(s) permanently deleted.`);
return res.data;
}),
[request]
+5 -30
View File
@@ -13,12 +13,7 @@ export function AdminCategoriesProvider({ children }) {
setLoading(true);
try { return await fn(); }
catch (err) {
toast(err?.response?.data?.message ?? "Something went wrong.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Something went wrong.");
return null;
} finally { setLoading(false); }
}, []);
@@ -37,48 +32,28 @@ export function AdminCategoriesProvider({ children }) {
const createCategory = useCallback((payload) => wrap(async () => {
const { data } = await api.post("/admin/categories", payload);
toast("Category created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Category created.");
return data.data;
}), [wrap]);
const updateCategory = useCallback((id, payload) => wrap(async () => {
const { data } = await api.put(`/admin/categories/${id}`, payload);
setCategory(data.data ?? null);
toast("Category updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Category updated.");
return data.data;
}), [wrap]);
const archiveCategory = useCallback((id) => wrap(async () => {
await api.delete(`/admin/categories/${id}`);
setCategories((prev) => prev.filter((c) => c.id !== id));
toast("Category archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Category archived.");
return true;
}), [wrap]);
const restoreCategory = useCallback((id) => wrap(async () => {
await api.post(`/admin/categories/${id}/restore`);
setCategories((prev) => prev.filter((c) => c.id !== id));
toast("Category restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Category restored.");
return true;
}), [wrap]);
@@ -43,12 +43,7 @@ export function AdminCourseReadingProgressProvider({ children }) {
setProgressList(data.data ?? []);
setDetailCache({});
} catch (err) {
toast(err?.response?.data?.message ?? 'Could not load reading progress.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? 'Could not load reading progress.');
} finally {
setListLoading(false);
}
@@ -65,12 +60,7 @@ export function AdminCourseReadingProgressProvider({ children }) {
setDetailCache((prev) => ({ ...prev, [userId]: breakdown }));
return breakdown;
} catch (err) {
toast(err?.response?.data?.message ?? 'Could not load user progress.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? 'Could not load user progress.');
return null;
} finally {
setDetailLoading(false);
+55 -330
View File
@@ -52,18 +52,8 @@ export function CoursesProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? "Something went wrong.";
if (err.status === 404 && message) toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
else toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
if (err.status === 404 && message) toast(message);
else toast(message);
return null;
} finally {
setLoading(false);
@@ -121,12 +111,7 @@ export function CoursesProvider({ children }) {
const course = data?.data?.data ?? null;
if (course) {
setCourses((prev) => [course, ...prev]);
toast("Course created successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Course created successfully.");
}
return data;
}),
@@ -141,12 +126,7 @@ export function CoursesProvider({ children }) {
if (course) {
setCourses((prev) => prev.map((c) => (c.course_id === courseId ? course : c)));
setCourse(course);
toast("Course updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Course updated successfully.");
}
return data;
}),
@@ -159,12 +139,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}`);
setCourses((prev) => prev.filter((c) => c.course_id !== courseId));
setCourse((prev) => (prev?.course_id === courseId ? null : prev));
toast("Course archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Course archived.");
return data;
}),
[request],
@@ -175,12 +150,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/bulk`, { data: { ids } });
setCourses((prev) => prev.filter((c) => !ids.includes(c.course_id)));
toast("Courses archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Courses archived.");
return data;
}),
[request],
@@ -211,12 +181,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setCourses((prev) => prev.filter((c) => c.course_id !== courseId));
toast("Course restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Course restored.");
}
return data;
}),
@@ -228,12 +193,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.patch(`${BASE}/restore/bulk`, { ids });
setCourses((prev) => prev.filter((c) => !ids.includes(c.course_id)));
toast("Courses restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Courses restored.");
return data;
}),
[request],
@@ -245,12 +205,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}/permanent`);
setCourses((prev) => prev.filter((c) => c.course_id !== courseId));
setCourse((prev) => (prev?.course_id === courseId ? null : prev));
toast("Course permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Course permanently deleted.");
return data;
}),
[request],
@@ -261,12 +216,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/bulk/permanent`, { data: { ids } });
setCourses((prev) => prev.filter((c) => !ids.includes(c.course_id)));
toast("Courses permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Courses permanently deleted.");
return data;
}),
[request],
@@ -305,12 +255,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.put(`${BASE}/${courseId}/prerequisites`, { prerequisites });
setPrerequisites(prerequisites);
toast("Prerequisites updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Prerequisites updated.");
return data;
}),
[request],
@@ -360,12 +305,7 @@ export function CoursesProvider({ children }) {
const unit = data?.data?.data ?? null;
if (unit) {
setUnits((prev) => [...prev, unit]);
toast("Unit created successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Unit created successfully.");
}
return data;
}),
@@ -380,12 +320,7 @@ export function CoursesProvider({ children }) {
if (unit) {
setUnits((prev) => prev.map((u) => (u.unit_id === unitId ? unit : u)));
setUnit(unit);
toast("Unit updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Unit updated successfully.");
}
return data;
}),
@@ -398,12 +333,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}`);
setUnits((prev) => prev.filter((u) => u.unit_id !== unitId));
setUnit((prev) => (prev?.unit_id === unitId ? null : prev));
toast("Unit archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Unit archived.");
return data;
}),
[request],
@@ -414,12 +344,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/units/bulk`, { data: { ids } });
setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id)));
toast("Units archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Units archived.");
return data;
}),
[request],
@@ -451,12 +376,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setUnits((prev) => prev.filter((u) => u.unit_id !== unitId));
toast("Unit restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Unit restored.");
}
return data;
}),
@@ -468,12 +388,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.patch(`${BASE}/${courseId}/units/restore/bulk`, { ids } );
setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id)));
toast("Units restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Units restored.");
return data;
}),
[request],
@@ -485,12 +400,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/permanent`);
setUnits((prev) => prev.filter((u) => u.unit_id !== unitId));
setUnit((prev) => (prev?.unit_id === unitId ? null : prev));
toast("Unit permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Unit permanently deleted.");
return data;
}),
[request],
@@ -501,12 +411,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/units/bulk/permanent`, { data: { ids } });
setUnits((prev) => prev.filter((u) => !ids.includes(u.unit_id)));
toast("Units permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Units permanently deleted.");
return data;
}),
[request],
@@ -545,12 +450,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setQuiz(result);
toast("Quiz created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Quiz created.");
}
return data;
}),
@@ -564,12 +464,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setQuiz(result);
toast("Quiz updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Quiz updated.");
}
return data;
}),
@@ -582,12 +477,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}`, { data: { deletedBy } });
setQuiz(null);
setQuestions([]);
toast("Quiz archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Quiz archived.");
return data;
}),
[request],
@@ -613,12 +503,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setQuiz(result);
toast("Quiz restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Quiz restored.");
}
return data;
}),
@@ -647,12 +532,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setQuestions((prev) => [...prev, result]);
toast("Question added.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question added.");
}
return data;
}),
@@ -666,12 +546,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setQuestions((prev) => prev.map((q) => (q.question_id === questionId ? result : q)));
toast("Question updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question updated.");
}
return data;
}),
@@ -684,12 +559,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.put(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/bulk-sync`, { questions, updatedBy });
const result = data?.data?.data ?? [];
setQuestions(result);
toast("Quiz saved.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Quiz saved.");
return data;
}),
[request],
@@ -700,12 +570,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/${questionId}`, { data: { deletedBy } });
setQuestions((prev) => prev.filter((q) => q.question_id !== questionId));
toast("Question archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question archived.");
return data;
}),
[request],
@@ -716,12 +581,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/bulk`, { data: { ids, deletedBy } });
setQuestions((prev) => prev.filter((q) => !ids.includes(q.question_id)));
toast("Questions archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Questions archived.");
return data;
}),
[request],
@@ -742,12 +602,7 @@ export function CoursesProvider({ children }) {
(courseId, unitId, quizId, questionId, restoredBy) =>
request(async () => {
const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/${questionId}/restore`, { restoredBy });
toast("Question restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question restored.");
return data;
}),
[request],
@@ -757,12 +612,7 @@ export function CoursesProvider({ children }) {
(courseId, unitId, quizId, ids, restoredBy) =>
request(async () => {
const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/quiz/${quizId}/questions/restore/bulk`, { ids, restoredBy });
toast("Questions restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Questions restored.");
return data;
}),
[request],
@@ -797,12 +647,7 @@ export function CoursesProvider({ children }) {
const lesson = data?.data?.data ?? null;
if (lesson) {
setLessons((prev) => [...prev, lesson]);
toast("Lesson created successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lesson created successfully.");
}
return data;
}),
@@ -817,12 +662,7 @@ export function CoursesProvider({ children }) {
if (lesson) {
setLessons((prev) => prev.map((l) => (l.lesson_id === lessonId ? lesson : l)));
setLesson(lesson);
toast("Lesson updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lesson updated successfully.");
}
return data;
}),
@@ -835,12 +675,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}`);
setLessons((prev) => prev.filter((l) => l.lesson_id !== lessonId));
setLesson((prev) => (prev?.lesson_id === lessonId ? null : prev));
toast("Lesson archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lesson archived.");
return data;
}),
[request],
@@ -851,12 +686,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/bulk`, { data: { ids } });
setLessons((prev) => prev.filter((l) => !ids.includes(l.lesson_id)));
toast("Lessons archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lessons archived.");
return data;
}),
[request],
@@ -888,12 +718,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setLessons((prev) => prev.filter((l) => l.lesson_id !== lessonId));
toast("Lesson restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lesson restored.");
}
return data;
}),
@@ -905,12 +730,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.patch(`${BASE}/${courseId}/units/${unitId}/lessons/restore/bulk`, { ids });
setLessons((prev) => prev.filter((l) => !ids.includes(l.lesson_id)));
toast("Lessons restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lessons restored.");
return data;
}),
[request],
@@ -922,12 +742,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/${lessonId}/permanent`);
setLessons((prev) => prev.filter((l) => l.lesson_id !== lessonId));
setLesson((prev) => (prev?.lesson_id === lessonId ? null : prev));
toast("Lesson permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lesson permanently deleted.");
return data;
}),
[request],
@@ -938,12 +753,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/units/${unitId}/lessons/bulk/permanent`, { data: { ids } });
setLessons((prev) => prev.filter((l) => !ids.includes(l.lesson_id)));
toast("Lessons permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lessons permanently deleted.");
return data;
}),
[request],
@@ -971,12 +781,7 @@ export function CoursesProvider({ children }) {
const page = data?.data?.data ?? null;
if (page) {
setLessonPage(page);
toast("Lesson page saved.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Lesson page saved.");
}
return data;
}),
@@ -1006,12 +811,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setAssessment(result);
toast("Assessment created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Assessment created.");
}
return data;
}),
@@ -1025,12 +825,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setAssessment(result);
toast("Assessment updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Assessment updated.");
}
return data;
}),
@@ -1043,12 +838,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.delete(`${BASE}/${courseId}/assessment/${assessmentId}`, { data: { deletedBy } });
setAssessment(null);
setQuestions([]);
toast("Assessment archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Assessment archived.");
return data;
}),
[request],
@@ -1074,12 +864,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setAssessment(result);
toast("Assessment restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Assessment restored.");
}
return data;
}),
@@ -1108,12 +893,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setQuestions((prev) => [...prev, result]);
toast("Question added.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question added.");
}
return data;
}),
@@ -1127,12 +907,7 @@ export function CoursesProvider({ children }) {
const result = data?.data?.data ?? null;
if (result) {
setQuestions((prev) => prev.map((q) => (q.question_id === questionId ? result : q)));
toast("Question updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question updated.");
}
return data;
}),
@@ -1145,12 +920,7 @@ export function CoursesProvider({ children }) {
const { data } = await api.put(`${BASE}/${courseId}/assessment/${assessmentId}/questions/bulk-sync`, { questions, updatedBy });
const result = data?.data?.data ?? [];
setQuestions(result);
toast("Assessment saved.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Assessment saved.");
return data;
}),
[request],
@@ -1161,12 +931,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/assessment/${assessmentId}/questions/${questionId}`, { data: { deletedBy } });
setQuestions((prev) => prev.filter((q) => q.question_id !== questionId));
toast("Question archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question archived.");
return data;
}),
[request],
@@ -1177,12 +942,7 @@ export function CoursesProvider({ children }) {
request(async () => {
const { data } = await api.delete(`${BASE}/${courseId}/assessment/${assessmentId}/questions/bulk`, { data: { ids, deletedBy } });
setQuestions((prev) => prev.filter((q) => !ids.includes(q.question_id)));
toast("Questions archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Questions archived.");
return data;
}),
[request],
@@ -1203,12 +963,7 @@ export function CoursesProvider({ children }) {
(courseId, assessmentId, questionId, restoredBy) =>
request(async () => {
const { data } = await api.patch(`${BASE}/${courseId}/assessment/${assessmentId}/questions/${questionId}/restore`, { restoredBy });
toast("Question restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Question restored.");
return data;
}),
[request],
@@ -1218,12 +973,7 @@ export function CoursesProvider({ children }) {
(courseId, assessmentId, ids, restoredBy) =>
request(async () => {
const { data } = await api.patch(`${BASE}/${courseId}/assessment/${assessmentId}/questions/restore/bulk`, { ids, restoredBy });
toast("Questions restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Questions restored.");
return data;
}),
[request],
@@ -1280,12 +1030,7 @@ export function CoursesProvider({ children }) {
const saveCourseProduct = useCallback(
(courseId, payload) => request(async () => {
const { data } = await api.put(`/admin/products/courses/${courseId}/product`, payload);
toast("Product listing saved.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Product listing saved.");
return data.data ?? null;
}), [request],
);
@@ -1293,12 +1038,7 @@ export function CoursesProvider({ children }) {
const removeCourseProduct = useCallback(
(courseId) => request(async () => {
await api.delete(`/admin/products/courses/${courseId}/product`);
toast("Product listing removed.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Product listing removed.");
return true;
}), [request],
);
@@ -1313,12 +1053,7 @@ export function CoursesProvider({ children }) {
const syncCourseCategories = useCallback(
(courseId, categoryIds) => request(async () => {
const { data } = await api.post(`/admin/products/courses/${courseId}/categories`, { category_ids: categoryIds });
toast("Categories updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Categories updated.");
return data.data ?? [];
}), [request],
);
@@ -1333,12 +1068,7 @@ export function CoursesProvider({ children }) {
const syncInstructors = useCallback(
(courseId, instructors) => request(async () => {
await api.put(`${BASE}/${courseId}/instructors`, { instructors });
toast("Instructors updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Instructors updated.");
}), [request],
);
@@ -1352,12 +1082,7 @@ export function CoursesProvider({ children }) {
const syncCourseAchievements = useCallback(
(courseId, achievement_keys) => request(async () => {
await api.put(`${BASE}/${courseId}/achievements`, { achievement_keys });
toast("Rewards updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Rewards updated.");
}), [request],
);
+1 -6
View File
@@ -21,12 +21,7 @@ export function AdminDashboardProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? "Something went wrong.";
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -34,12 +34,7 @@ export function NotificationBroadcastsProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? "Something went wrong.";
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -105,12 +100,7 @@ export function NotificationBroadcastsProvider({ children }) {
const broadcast = res.data?.data?.data ?? null;
if (broadcast) {
setBroadcasts((prev) => [broadcast, ...prev]);
toast("Notification broadcast created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Notification broadcast created.");
}
return res.data;
}),
@@ -126,12 +116,7 @@ export function NotificationBroadcastsProvider({ children }) {
if (broadcast) {
setBroadcasts((prev) => prev.map((b) => (b.broadcast_id === broadcastId ? broadcast : b)));
setSelectedBroadcast(broadcast);
toast("Notification broadcast updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Notification broadcast updated.");
}
return res.data;
}),
@@ -147,12 +132,7 @@ export function NotificationBroadcastsProvider({ children }) {
if (broadcast) {
setBroadcasts((prev) => prev.map((b) => (b.broadcast_id === broadcastId ? broadcast : b)));
setSelectedBroadcast(broadcast);
toast("Notification broadcast sent.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Notification broadcast sent.");
}
return res.data;
}),
@@ -168,12 +148,7 @@ export function NotificationBroadcastsProvider({ children }) {
});
setBroadcasts((prev) => prev.filter((b) => b.broadcast_id !== broadcastId));
setSelectedBroadcast((prev) => (prev?.broadcast_id === broadcastId ? null : prev));
toast("Notification broadcast archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Notification broadcast archived.");
return res.data;
}),
[request]
@@ -187,12 +162,7 @@ export function NotificationBroadcastsProvider({ children }) {
data: { ids, deletedBy },
});
setBroadcasts((prev) => prev.filter((b) => !ids.includes(b.broadcast_id)));
toast(`${ids.length} notification broadcast(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} notification broadcast(s) archived.`);
return res.data;
}),
[request]
@@ -206,12 +176,7 @@ export function NotificationBroadcastsProvider({ children }) {
const broadcast = res.data?.data?.data ?? null;
if (broadcast) {
setBroadcasts((prev) => prev.filter((b) => b.broadcast_id !== broadcastId));
toast("Notification broadcast restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Notification broadcast restored.");
}
return res.data;
}),
@@ -224,12 +189,7 @@ export function NotificationBroadcastsProvider({ children }) {
request(async () => {
const res = await api.patch("/admin/notification-broadcasts/bulk-restore", { ids });
setBroadcasts((prev) => prev.filter((b) => !ids.includes(b.broadcast_id)));
toast(`${ids.length} notification broadcast(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} notification broadcast(s) restored.`);
return res.data;
}),
[request]
@@ -241,12 +201,7 @@ export function NotificationBroadcastsProvider({ children }) {
request(async () => {
const res = await api.delete(`/admin/notification-broadcasts/${broadcastId}/permanent`);
setBroadcasts((prev) => prev.filter((b) => b.broadcast_id !== broadcastId));
toast("Notification broadcast permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Notification broadcast permanently deleted.");
return res.data;
}),
[request]
@@ -258,12 +213,7 @@ export function NotificationBroadcastsProvider({ children }) {
request(async () => {
const res = await api.delete("/admin/notification-broadcasts/bulk/permanent", { data: { ids } });
setBroadcasts((prev) => prev.filter((b) => !ids.includes(b.broadcast_id)));
toast(`${ids.length} notification broadcast(s) permanently deleted.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} notification broadcast(s) permanently deleted.`);
return res.data;
}),
[request]
@@ -19,12 +19,7 @@ export function AdminNotificationTemplateProvider({ children }) {
setLoading(true);
try { return await fn(); }
catch (err) {
toast(err?.response?.data?.message ?? "Something went wrong.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Something went wrong.");
return null;
} finally { setLoading(false); }
}, []);
@@ -50,12 +45,7 @@ export function AdminNotificationTemplateProvider({ children }) {
prev.map((t) => (String(t.notification_template_id) === String(id) ? data.data : t))
);
if (template && String(template.notification_template_id) === String(id)) setTemplate(data.data);
toast("Notification template updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Notification template updated.");
return data.data;
}), [request, template]);
+25 -150
View File
@@ -57,12 +57,7 @@ export function AdminTaskProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? 'Something went wrong.';
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -75,12 +70,7 @@ export function AdminTaskProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? 'Something went wrong.';
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setCompletionLoading(false);
@@ -143,12 +133,7 @@ export function AdminTaskProvider({ children }) {
(payload) =>
request(async () => {
const res = await api.post(BASE, payload);
toast('Task list created.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task list created.');
return res.data?.data ?? null;
}),
[request]
@@ -158,12 +143,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, payload) =>
request(async () => {
const res = await api.patch(`${BASE}/${taskListId}`, payload);
toast('Task list updated.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task list updated.');
return res.data?.data ?? null;
}),
[request]
@@ -173,12 +153,7 @@ export function AdminTaskProvider({ children }) {
(taskListId) =>
request(async () => {
await api.delete(`${BASE}/${taskListId}`);
toast('Task list archived.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task list archived.');
return true;
}),
[request]
@@ -188,12 +163,7 @@ export function AdminTaskProvider({ children }) {
(taskListId) =>
request(async () => {
await api.patch(`${BASE}/${taskListId}/restore`);
toast('Task list restored.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task list restored.');
return true;
}),
[request]
@@ -203,12 +173,7 @@ export function AdminTaskProvider({ children }) {
(ids) =>
request(async () => {
await api.post(`${BASE}/bulk-archive`, { ids });
toast(`${ids.length} task list(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} task list(s) archived.`);
return true;
}),
[request]
@@ -218,12 +183,7 @@ export function AdminTaskProvider({ children }) {
(ids) =>
request(async () => {
await api.post(`${BASE}/bulk-restore`, { ids });
toast(`${ids.length} task list(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} task list(s) restored.`);
return true;
}),
[request]
@@ -233,12 +193,7 @@ export function AdminTaskProvider({ children }) {
(taskListId) =>
request(async () => {
await api.delete(`${BASE}/${taskListId}/permanent`);
toast('Task list permanently deleted.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task list permanently deleted.');
return true;
}),
[request]
@@ -248,12 +203,7 @@ export function AdminTaskProvider({ children }) {
(ids) =>
request(async () => {
await api.post(`${BASE}/bulk-delete`, { ids });
toast(`${ids.length} task list(s) permanently deleted.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} task list(s) permanently deleted.`);
return true;
}),
[request]
@@ -304,19 +254,9 @@ export function AdminTaskProvider({ children }) {
});
const result = res.data?.data ?? {};
if (result.assigned_ids?.length) {
toast(`${result.assigned_ids.length} group(s) assigned.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${result.assigned_ids.length} group(s) assigned.`);
} else {
toast('All selected groups were already assigned.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('All selected groups were already assigned.');
}
return result;
}),
@@ -330,12 +270,7 @@ export function AdminTaskProvider({ children }) {
group_ids: groupIds,
});
const result = res.data?.data ?? {};
toast(`${result.unassigned_ids?.length ?? 0} group(s) unassigned.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${result.unassigned_ids?.length ?? 0} group(s) unassigned.`);
return result;
}),
[request]
@@ -396,12 +331,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, payload) =>
request(async () => {
const res = await api.post(`${BASE}/${taskListId}/tasks`, payload);
toast('Task created.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task created.');
return res.data?.data?.data ?? null;
}),
[request]
@@ -411,12 +341,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, taskId, payload) =>
request(async () => {
const res = await api.patch(`${BASE}/${taskListId}/tasks/${taskId}`, payload);
toast('Task updated.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task updated.');
return res.data?.data?.data ?? null;
}),
[request]
@@ -426,12 +351,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, taskId) =>
request(async () => {
await api.delete(`${BASE}/${taskListId}/tasks/${taskId}`);
toast('Task archived.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task archived.');
return true;
}),
[request]
@@ -441,12 +361,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, taskId) =>
request(async () => {
await api.patch(`${BASE}/${taskListId}/tasks/${taskId}/restore`);
toast('Task restored.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task restored.');
return true;
}),
[request]
@@ -456,12 +371,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, ids) =>
request(async () => {
await api.post(`${BASE}/${taskListId}/tasks/bulk-archive`, { ids });
toast(`${ids.length} task(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} task(s) archived.`);
return true;
}),
[request]
@@ -471,12 +381,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, ids) =>
request(async () => {
await api.post(`${BASE}/${taskListId}/tasks/bulk-restore`, { ids });
toast(`${ids.length} task(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} task(s) restored.`);
return true;
}),
[request]
@@ -486,12 +391,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, taskId) =>
request(async () => {
await api.delete(`${BASE}/${taskListId}/tasks/${taskId}/permanent`);
toast('Task permanently deleted.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task permanently deleted.');
return true;
}),
[request]
@@ -501,12 +401,7 @@ export function AdminTaskProvider({ children }) {
(taskListId, ids) =>
request(async () => {
await api.post(`${BASE}/${taskListId}/tasks/bulk-delete`, { ids });
toast(`${ids.length} task(s) permanently deleted.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} task(s) permanently deleted.`);
return true;
}),
[request]
@@ -625,12 +520,7 @@ export function AdminTaskProvider({ children }) {
await api.delete(
`${BASE}/${taskListId}/tasks/${taskId}/completions/${completionId}`
);
toast('Completion archived.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Completion archived.');
return true;
}),
[completionRequest]
@@ -643,12 +533,7 @@ export function AdminTaskProvider({ children }) {
await api.patch(
`${BASE}/${taskListId}/tasks/${taskId}/completions/${completionId}/restore`
);
toast('Completion restored.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Completion restored.');
return true;
}),
[completionRequest]
@@ -662,12 +547,7 @@ export function AdminTaskProvider({ children }) {
`${BASE}/${taskListId}/tasks/${taskId}/completions/bulk-archive`,
{ ids }
);
toast(`${ids.length} completion(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} completion(s) archived.`);
return true;
}),
[completionRequest]
@@ -681,12 +561,7 @@ export function AdminTaskProvider({ children }) {
`${BASE}/${taskListId}/tasks/${taskId}/completions/bulk-restore`,
{ ids }
);
toast(`${ids.length} completion(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${ids.length} completion(s) restored.`);
return true;
}),
[completionRequest]
+4 -24
View File
@@ -19,12 +19,7 @@ export function AdminTierCategoriesProvider({ children }) {
setLoading(true);
try { return await fn(); }
catch (err) {
toast(err?.response?.data?.message ?? "Something went wrong.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Something went wrong.");
return null;
} finally { setLoading(false); }
}, []);
@@ -46,12 +41,7 @@ export function AdminTierCategoriesProvider({ children }) {
const createCategory = useCallback((payload) =>
request(async () => {
const { data } = await api.post("/admin/tiers/categories", payload);
toast("Tier category created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Tier category created.");
return data.data;
}), [request]);
@@ -62,12 +52,7 @@ export function AdminTierCategoriesProvider({ children }) {
prev.map((c) => (String(c.tier_category_id) === String(id) ? data.data : c))
);
if (category && String(category.tier_category_id) === String(id)) setCategory(data.data);
toast("Tier category updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Tier category updated.");
return data.data;
}), [request, category]);
@@ -75,12 +60,7 @@ export function AdminTierCategoriesProvider({ children }) {
request(async () => {
await api.delete(`/admin/tiers/categories/${id}`);
setCategories((prev) => prev.filter((c) => String(c.tier_category_id) !== String(id)));
toast("Tier category deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Tier category deleted.");
return true;
}), [request]);
+2 -12
View File
@@ -20,12 +20,7 @@ export function AdminTierPoliciesProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? "Something went wrong.";
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -50,12 +45,7 @@ export function AdminTierPoliciesProvider({ children }) {
? prev.map((b) => (b.key === key ? data.data : b))
: [...prev, data.data];
});
toast("Badge saved.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Badge saved.");
return data.data;
}), [request]);
+25 -150
View File
@@ -33,12 +33,7 @@ export function AdminTiersProvider({ children }) {
totalPages: data.data?.pagination?.totalPages ?? 1,
totalRecords: data.data?.pagination?.totalRecords ?? 0,
});
} catch { toast("Could not load plans.", {
action: {
label: "Close",
onClick: () => {}
}
}); }
} catch { toast("Could not load plans."); }
finally { setLoading(false); }
}, []);
@@ -47,12 +42,7 @@ export function AdminTiersProvider({ children }) {
try {
const { data } = await api.get(`/admin/tiers/${id}`);
setPlan(data.data ?? null);
} catch { toast("Could not load plan.", {
action: {
label: "Close",
onClick: () => {}
}
}); }
} catch { toast("Could not load plan."); }
finally { setLoading(false); }
}, []);
@@ -60,20 +50,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
const { data } = await api.post("/admin/tiers", payload);
toast("Plan created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plan created.");
return data.data;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not create plan.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not create plan.");
return null;
} finally { setLoading(false); }
}, []);
@@ -82,20 +62,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
const { data } = await api.put(`/admin/tiers/${id}`, payload);
toast("Plan updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plan updated.");
return data.data;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not update plan.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not update plan.");
return null;
} finally { setLoading(false); }
}, []);
@@ -104,20 +74,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.delete(`/admin/tiers/${id}`);
toast("Plan archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plan archived.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not archive plan.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not archive plan.");
return false;
} finally { setLoading(false); }
}, []);
@@ -126,20 +86,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.post(`/admin/tiers/${id}/restore`);
toast("Plan restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plan restored.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not restore plan.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not restore plan.");
return false;
} finally { setLoading(false); }
}, []);
@@ -148,20 +98,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.post("/admin/tiers/bulk/archive", { ids });
toast("Plans archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plans archived.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not archive plans.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not archive plans.");
return false;
} finally { setLoading(false); }
}, []);
@@ -170,20 +110,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.post("/admin/tiers/bulk/restore", { ids });
toast("Plans restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plans restored.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not restore plans.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not restore plans.");
return false;
} finally { setLoading(false); }
}, []);
@@ -192,20 +122,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.delete(`/admin/tiers/${id}/permanent`);
toast("Plan permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plan permanently deleted.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not permanently delete plan.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not permanently delete plan.");
return false;
} finally { setLoading(false); }
}, []);
@@ -214,20 +134,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.post("/admin/tiers/bulk/permanent-delete", { ids });
toast("Plans permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Plans permanently deleted.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not permanently delete plans.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not permanently delete plans.");
return false;
} finally { setLoading(false); }
}, []);
@@ -252,12 +162,7 @@ export function AdminTiersProvider({ children }) {
try {
const { data } = await api.get(`/admin/tiers/users/${userId}/tiers`);
setUserTiers(data.data ?? []);
} catch { toast("Could not load user tiers.", {
action: {
label: "Close",
onClick: () => {}
}
}); }
} catch { toast("Could not load user tiers."); }
finally { setLoading(false); }
}, []);
@@ -265,20 +170,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.post("/admin/tiers/users/tiers/grant", payload);
toast("Tier granted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Tier granted.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not grant tier.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not grant tier.");
return false;
} finally { setLoading(false); }
}, []);
@@ -287,20 +182,10 @@ export function AdminTiersProvider({ children }) {
setLoading(true);
try {
await api.patch(`/admin/tiers/users/tiers/${tid}/revoke`);
toast("Tier revoked.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Tier revoked.");
return true;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not revoke tier.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not revoke tier.");
return false;
} finally { setLoading(false); }
}, []);
@@ -321,12 +206,7 @@ export function AdminTiersProvider({ children }) {
totalPages: data.data?.pagination?.totalPages ?? 1,
totalRecords: data.data?.pagination?.totalRecords ?? 0,
});
} catch { toast("Could not load payments.", {
action: {
label: "Close",
onClick: () => {}
}
}); }
} catch { toast("Could not load payments."); }
finally { setLoading(false); }
}, []);
@@ -335,12 +215,7 @@ export function AdminTiersProvider({ children }) {
try {
const { data } = await api.get(`/admin/tiers/payments/${id}`);
setPayment(data.data ?? null);
} catch { toast("Could not load payment.", {
action: {
label: "Close",
onClick: () => {}
}
}); }
} catch { toast("Could not load payment."); }
finally { setLoading(false); }
}, []);
+17 -102
View File
@@ -45,12 +45,7 @@ export const UserProvider = ({ children }) => {
} catch (err) {
const message = err?.response?.data?.message || err.message || "Something went wrong.";
setError(message);
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -113,12 +108,7 @@ export const UserProvider = ({ children }) => {
(payload) =>
request(async () => {
const res = await api.post(`${BASE}/users/staff`, payload);
toast("Staff user added successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Staff user added successfully.");
return res.data;
}),
[request]
@@ -132,12 +122,7 @@ export const UserProvider = ({ children }) => {
setUsers((prev) =>
prev.map((u) => (u.user_id === userId ? { ...u, ...res.data?.data } : u))
);
toast("User updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("User updated successfully.");
return res.data;
}),
[request]
@@ -151,12 +136,7 @@ export const UserProvider = ({ children }) => {
setUsers((prev) =>
prev.map((u) => (u.user_id === userId ? { ...u, is_active: false } : u))
);
toast("User deactivated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("User deactivated.");
return res.data;
}),
[request]
@@ -174,12 +154,7 @@ export const UserProvider = ({ children }) => {
deactivated_ids.includes(u.user_id) ? { ...u, is_active: false } : u
)
);
toast(`${deactivated_ids.length} user(s) deactivated.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${deactivated_ids.length} user(s) deactivated.`);
}
return res.data;
}),
@@ -194,12 +169,7 @@ export const UserProvider = ({ children }) => {
setUsers((prev) =>
prev.map((u) => (u.user_id === userId ? { ...u, is_active: true } : u))
);
toast("User restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("User restored.");
return res.data;
}),
[request]
@@ -217,12 +187,7 @@ export const UserProvider = ({ children }) => {
restored_ids.includes(u.user_id) ? { ...u, is_active: true } : u
)
);
toast(`${restored_ids.length} user(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${restored_ids.length} user(s) restored.`);
}
return res.data;
}),
@@ -235,12 +200,7 @@ export const UserProvider = ({ children }) => {
request(async () => {
const res = await api.delete(`${BASE}/users/${userId}/permanent`);
setUsers((prev) => prev.filter((u) => u.user_id !== userId));
toast("User permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("User permanently deleted.");
return res.data;
}),
[request]
@@ -254,12 +214,7 @@ export const UserProvider = ({ children }) => {
const { deleted_ids } = res.data?.data ?? {};
if (deleted_ids?.length) {
setUsers((prev) => prev.filter((u) => !deleted_ids.includes(u.user_id)));
toast(`${deleted_ids.length} user(s) permanently deleted.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${deleted_ids.length} user(s) permanently deleted.`);
}
return res.data;
}),
@@ -285,12 +240,7 @@ export const UserProvider = ({ children }) => {
setSessions((prev) =>
prev.map((s) => (s.session_id === sessionId ? { ...s, is_active: false } : s))
);
toast("Session terminated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Session terminated.");
return res.data;
}),
[request]
@@ -334,12 +284,7 @@ export const UserProvider = ({ children }) => {
});
return d;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load activity.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load activity.");
return null;
} finally {
setActivityLoading(false);
@@ -366,12 +311,7 @@ export const UserProvider = ({ children }) => {
setAchievements(res.data?.data ?? []);
return res.data?.data;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load achievements.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load achievements.");
return [];
} finally {
setAchievementsLoading(false);
@@ -387,12 +327,7 @@ export const UserProvider = ({ children }) => {
prev.map((u) => (u.user_id === userId ? { ...u, is_banned: true } : u))
);
if (user?.user_id === userId) setUser((prev) => prev ? { ...prev, is_banned: true } : prev);
toast("User banned successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("User banned successfully.");
return res.data;
}),
[request, user]
@@ -407,12 +342,7 @@ export const UserProvider = ({ children }) => {
prev.map((u) => (u.user_id === userId ? { ...u, is_banned: false } : u))
);
if (user?.user_id === userId) setUser((prev) => prev ? { ...prev, is_banned: false } : prev);
toast("User unbanned successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("User unbanned successfully.");
return res.data;
}),
[request, user]
@@ -428,12 +358,7 @@ export const UserProvider = ({ children }) => {
setUsers((prev) =>
prev.map((u) => (banned_ids.includes(u.user_id) ? { ...u, is_banned: true } : u))
);
toast(`${banned_ids.length} user(s) banned.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${banned_ids.length} user(s) banned.`);
}
return res.data;
}),
@@ -450,12 +375,7 @@ export const UserProvider = ({ children }) => {
setUsers((prev) =>
prev.map((u) => (unbanned_ids.includes(u.user_id) ? { ...u, is_banned: false } : u))
);
toast(`${unbanned_ids.length} user(s) unbanned.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${unbanned_ids.length} user(s) unbanned.`);
}
return res.data;
}),
@@ -470,12 +390,7 @@ export const UserProvider = ({ children }) => {
setBans(res.data?.data ?? []);
return res.data?.data;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load ban history.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load ban history.");
return [];
} finally {
setBansLoading(false);
+11 -66
View File
@@ -27,12 +27,7 @@ export function UserGroupProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? "Something went wrong.";
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -126,12 +121,7 @@ export function UserGroupProvider({ children }) {
request(async () => {
const res = await api.post(`${BASE}/groups`, { name, description, group_code });
setGroups((prev) => [res.data?.data, ...prev]);
toast("Group created successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Group created successfully.");
return res.data;
}),
[request]
@@ -146,12 +136,7 @@ export function UserGroupProvider({ children }) {
prev.map((g) => (g.group_id === gid ? { ...g, ...res.data?.data } : g))
);
setGroup((prev) => (prev?.group_id === gid ? { ...prev, ...res.data?.data } : prev));
toast("Group updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Group updated successfully.");
return res.data;
}),
[request]
@@ -165,12 +150,7 @@ export function UserGroupProvider({ children }) {
setGroups((prev) =>
prev.map((g) => (g.group_id === gid ? { ...g, is_active: false } : g))
);
toast("Group deactivated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Group deactivated.");
return res.data;
}),
[request]
@@ -188,12 +168,7 @@ export function UserGroupProvider({ children }) {
deactivated_ids.includes(g.group_id) ? { ...g, is_active: false } : g
)
);
toast(`${deactivated_ids.length} group(s) deactivated.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${deactivated_ids.length} group(s) deactivated.`);
}
return res.data;
}),
@@ -208,12 +183,7 @@ export function UserGroupProvider({ children }) {
setGroups((prev) =>
prev.map((g) => (g.group_id === gid ? { ...g, is_active: true } : g))
);
toast("Group restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Group restored.");
return res.data;
}),
[request]
@@ -231,12 +201,7 @@ export function UserGroupProvider({ children }) {
restored_ids.includes(g.group_id) ? { ...g, is_active: true } : g
)
);
toast(`${restored_ids.length} group(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${restored_ids.length} group(s) restored.`);
}
return res.data;
}),
@@ -249,12 +214,7 @@ export function UserGroupProvider({ children }) {
request(async () => {
const res = await api.delete(`${BASE}/groups/${gid}/permanent`);
setGroups((prev) => prev.filter((g) => g.group_id !== gid));
toast("Group permanently deleted.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Group permanently deleted.");
return res.data;
}),
[request]
@@ -268,12 +228,7 @@ export function UserGroupProvider({ children }) {
const { deleted_ids } = res.data?.data ?? {};
if (deleted_ids?.length) {
setGroups((prev) => prev.filter((g) => !deleted_ids.includes(g.group_id)));
toast(`${deleted_ids.length} group(s) permanently deleted.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${deleted_ids.length} group(s) permanently deleted.`);
}
return res.data;
}),
@@ -286,12 +241,7 @@ export function UserGroupProvider({ children }) {
request(async () => {
const res = await api.post(`${BASE}/groups/${gid}/users`, { user_ids });
setUsersNotIn((prev) => prev.filter((u) => !user_ids.includes(u.user_id)));
toast("Users added to group.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Users added to group.");
return res.data;
}),
[request]
@@ -304,12 +254,7 @@ export function UserGroupProvider({ children }) {
const res = await api.delete(`${BASE}/groups/${gid}/users`, { data: { user_ids } });
setMembers((prev) => prev.filter((u) => !user_ids.includes(u.user_id)));
setUsersIn((prev) => prev.filter((u) => !user_ids.includes(u.user_id)));
toast("Users removed from group.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Users removed from group.");
return res.data;
}),
[request]
+13
View File
@@ -97,6 +97,18 @@ export function AuthProvider({ children }) {
return { success: true, email: data.data.email }
} catch (err) {
const message = err.response?.data?.message || 'Could not process request.'
const isGoogleAccount = err.response?.data?.errors?.google === true
return { success: false, message, isGoogleAccount }
}
}, [])
// ── Verify reset OTP (step 2 — does not change the password) ──────────────
const verifyResetOtp = useCallback(async ({ email, otp }) => {
try {
await api.post('/auth/verify-reset-otp', { email, otp })
return { success: true }
} catch (err) {
const message = err.response?.data?.message || 'Verification failed.'
return { success: false, message }
}
}, [])
@@ -158,6 +170,7 @@ export function AuthProvider({ children }) {
verifyOTP,
resendOTP,
forgotPassword,
verifyResetOtp,
resetPassword,
logout,
loading,
@@ -58,12 +58,7 @@ export function CourseReadingProgressProvider({ children }) {
);
return rows;
} catch (err) {
toast(err?.response?.data?.message ?? 'Could not load course progress.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? 'Could not load course progress.');
return null;
} finally {
setLoading(false);
@@ -106,12 +101,7 @@ export function CourseReadingProgressProvider({ children }) {
delete next[lessonUuid];
return next;
});
toast(err?.response?.data?.message ?? 'Could not update progress.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? 'Could not update progress.');
return null;
}
}, []);
+13 -78
View File
@@ -42,12 +42,7 @@ export function ClientCoursesProvider({ children }) {
const { data } = await api.get("/client/courses");
setCourses(data.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load courses.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load courses.");
} finally {
setCoursesLoading(false);
}
@@ -63,12 +58,7 @@ export function ClientCoursesProvider({ children }) {
if (err?.response?.status === 403) {
setCourseBlocked(true); // let the UI show an upgrade prompt
} else {
toast(err?.response?.data?.message ?? "Could not load course.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load course.");
}
} finally {
setCourseLoading(false);
@@ -81,12 +71,7 @@ export function ClientCoursesProvider({ children }) {
const { data } = await api.get(`/client/courses/${courseId}/units/${unitId}`);
setUnit(data.data ?? null);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load unit.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load unit.");
} finally {
setUnitLoading(false);
}
@@ -100,12 +85,7 @@ export function ClientCoursesProvider({ children }) {
);
setLesson(data.data ?? null);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load lesson.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load lesson.");
} finally {
setLessonLoading(false);
}
@@ -119,12 +99,7 @@ export function ClientCoursesProvider({ children }) {
);
setQuiz(data.data ?? null);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load quiz.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load quiz.");
} finally {
setQuizLoading(false);
}
@@ -136,12 +111,7 @@ export function ClientCoursesProvider({ children }) {
const { data } = await api.get(`/client/courses/${courseId}/assessment`);
setAssessment(data.data ?? null);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load assessment.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load assessment.");
} finally {
setAssessmentLoading(false);
}
@@ -155,12 +125,7 @@ export function ClientCoursesProvider({ children }) {
);
return data.data ?? null;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not submit quiz.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not submit quiz.");
return null;
}
}, []);
@@ -170,12 +135,7 @@ export function ClientCoursesProvider({ children }) {
const { data } = await api.post(`/client/courses/${courseId}/assessment/${assessmentId}/start`);
return data.data ?? null;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not start assessment.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not start assessment.");
return null;
}
}, []);
@@ -207,12 +167,7 @@ export function ClientCoursesProvider({ children }) {
);
return data.data ?? null;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not submit assessment.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not submit assessment.");
return null;
}
}, []);
@@ -229,12 +184,7 @@ export function ClientCoursesProvider({ children }) {
const { data } = await api.get("/client/course-purchases");
setPurchases(data.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load purchases.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load purchases.");
} finally { setPurchasesLoading(false); }
}, []);
@@ -244,12 +194,7 @@ export function ClientCoursesProvider({ children }) {
const { data } = await api.post("/client/course-purchases/order", { product_id: productId });
return data.data ?? null;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not create order.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not create order.");
return null;
} finally { setPurchaseLoading(false); }
}, []);
@@ -258,20 +203,10 @@ export function ClientCoursesProvider({ children }) {
setPurchaseLoading(true);
try {
const { data } = await api.post("/client/course-purchases/capture", { order_id: orderId });
toast("Purchase confirmed! You now have access to this course.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Purchase confirmed! You now have access to this course.");
return data.data ?? null;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not capture payment.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not capture payment.");
return null;
} finally { setPurchaseLoading(false); }
}, []);
+1 -6
View File
@@ -30,12 +30,7 @@ export function GroupProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? 'Something went wrong.';
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
+3 -18
View File
@@ -45,12 +45,7 @@ export function TaskProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? 'Something went wrong.';
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -63,12 +58,7 @@ export function TaskProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? 'Something went wrong.';
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setCompletionLoading(false);
@@ -165,12 +155,7 @@ export function TaskProvider({ children }) {
payload
);
const data = res.data?.data ?? null;
toast('Task submitted successfully.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Task submitted successfully.');
// Immediately update latest completion so UI reflects the new state
setLatestCompletion(data);
// Prepend to history if it's already loaded
+1 -6
View File
@@ -45,12 +45,7 @@ export function TaskProgressProvider({ children }) {
return await fn();
} catch (err) {
const message = err?.response?.data?.message ?? 'Something went wrong.';
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
+8 -48
View File
@@ -44,20 +44,10 @@ export function ClientTiersProvider({ children }) {
// known-active tier — this prevents the "Free" flash after an upgrade.
setMyTier(prev => (tier === null && prev?.status === 'active') ? prev : tier);
if (tier?.just_expired) {
toast("Your subscription has expired. You've been moved to the Free plan.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Your subscription has expired. You've been moved to the Free plan.");
}
} catch (err) {
if (!silent) toast(err?.response?.data?.message ?? "Could not load tier.", {
action: {
label: "Close",
onClick: () => {}
}
});
if (!silent) toast(err?.response?.data?.message ?? "Could not load tier.");
} finally {
if (!silent) setTierLoading(false);
}
@@ -88,12 +78,7 @@ export function ClientTiersProvider({ children }) {
const { data } = await api.get("/client/tiers/me/history");
setTierHistory(data.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load tier history.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load tier history.");
} finally {
setTierHistoryLoading(false);
}
@@ -105,12 +90,7 @@ export function ClientTiersProvider({ children }) {
const { data } = await api.get("/client/tiers/plans");
setPlans(data.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load plans.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load plans.");
} finally {
setPlansLoading(false);
}
@@ -138,12 +118,7 @@ export function ClientTiersProvider({ children }) {
const { data } = await api.post("/client/tiers/checkout/order", payload);
return data.data ?? null;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not create order.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not create order.");
return null;
} finally {
setCheckoutLoading(false);
@@ -155,24 +130,14 @@ export function ClientTiersProvider({ children }) {
setCheckoutLoading(true);
try {
const { data } = await api.post("/client/tiers/checkout/capture", { order_id });
toast(data.message ?? "Payment successful. Tier activated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(data.message ?? "Payment successful. Tier activated.");
setMyTier({ tier: data.data?.tier, expires_at: data.data?.expires_at, status: "active" });
// Refresh from server so the browser cache holds fresh Premium data — prevents
// subsequent getMyTier() calls from getting a stale 304 with the old Free/null response.
getMyTier({ silent: true });
return data.data ?? null;
} catch (err) {
toast(err?.response?.data?.message ?? "Payment capture failed.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Payment capture failed.");
return null;
} finally {
setCheckoutLoading(false);
@@ -197,12 +162,7 @@ export function ClientTiersProvider({ children }) {
const { data } = await api.get("/client/tiers/me/payments");
setPayments(data.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load payment history.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load payment history.");
} finally {
setPaymentsLoading(false);
}
+11 -66
View File
@@ -34,12 +34,7 @@ export function ProfileProvider({ children, apiBase = '/client' }) {
setProfile(fresh);
return fresh;
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load profile.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load profile.");
return null;
} finally {
setProfileLoading(false);
@@ -52,20 +47,10 @@ export function ProfileProvider({ children, apiBase = '/client' }) {
const { data } = await api.put(`${apiBase}/profile`, { personal_info });
setProfile(data.data ?? null);
setUser((prev) => ({ ...prev, ...data.data }));
toast(data.message ?? "Profile updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(data.message ?? "Profile updated.");
return { success: true, data: data.data };
} catch (err) {
toast(err?.response?.data?.message ?? "Could not update profile.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not update profile.");
return { success: false };
} finally {
setProfileLoading(false);
@@ -78,12 +63,7 @@ export function ProfileProvider({ children, apiBase = '/client' }) {
const { data } = await api.get(`${apiBase}/sessions`);
setSessions(data.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load sessions.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load sessions.");
} finally {
setSessionsLoading(false);
}
@@ -94,20 +74,10 @@ export function ProfileProvider({ children, apiBase = '/client' }) {
try {
await api.delete(`${apiBase}/sessions/${sessionId}`);
setSessions((prev) => prev.filter((s) => s.session_id !== sessionId));
toast("Session revoked.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Session revoked.");
return { success: true };
} catch (err) {
toast(err?.response?.data?.message ?? "Could not revoke session.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not revoke session.");
return { success: false };
} finally {
setRevokingId(null);
@@ -122,20 +92,10 @@ export function ProfileProvider({ children, apiBase = '/client' }) {
const { data } = await api.post(`${apiBase}/profile/avatar`, formData);
setProfile(data.data ?? null);
setUser((prev) => ({ ...prev, personal_info: data.data?.personal_info }));
toast('Avatar updated.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Avatar updated.');
return { success: true };
} catch (err) {
toast(err?.response?.data?.message ?? 'Could not update avatar.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? 'Could not update avatar.');
return { success: false };
} finally {
setAvatarLoading(false);
@@ -154,20 +114,10 @@ export function ProfileProvider({ children, apiBase = '/client' }) {
...prev,
personal_info: { ...(prev?.personal_info ?? {}), avatar: null },
}));
toast('Avatar removed.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Avatar removed.');
return { success: true };
} catch (err) {
toast(err?.response?.data?.message ?? 'Could not remove avatar.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? 'Could not remove avatar.');
return { success: false };
} finally {
setAvatarLoading(false);
@@ -180,12 +130,7 @@ export function ProfileProvider({ children, apiBase = '/client' }) {
const { data } = await api.get(`${apiBase}/achievements`);
setAchievements(data.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not load achievements.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load achievements.");
} finally {
setAchievementsLoading(false);
}
+2 -12
View File
@@ -41,12 +41,7 @@ export const StaffGroupProvider = ({ children }) => {
} catch (err) {
const message = err?.response?.data?.message || err.message || "Something went wrong.";
setError(message);
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -61,12 +56,7 @@ export const StaffGroupProvider = ({ children }) => {
return await fn();
} catch (err) {
const message = err?.response?.data?.message || err.message || "Something went wrong.";
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setMembersLoading(false);
+1 -6
View File
@@ -27,12 +27,7 @@ export const StaffScoreProvider = ({ children }) => {
} catch (err) {
const message = err?.response?.data?.message || err.message || "Something went wrong.";
setError(message);
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
+13 -78
View File
@@ -53,12 +53,7 @@ export const StaffTaskProvider = ({ children }) => {
} catch (err) {
const message = err?.response?.data?.message || err.message || "Something went wrong.";
setError(message);
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
@@ -139,12 +134,7 @@ export const StaffTaskProvider = ({ children }) => {
const res = await api.post(`${BASE}/task-lists`, payload);
const created = res.data?.data;
if (created) setTaskLists((prev) => [created, ...prev]);
toast("Task list created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task list created.");
return res.data;
}),
[request]
@@ -163,12 +153,7 @@ export const StaffTaskProvider = ({ children }) => {
if (taskList?.task_list_id === taskListId)
setTaskList((prev) => ({ ...prev, ...updated }));
}
toast("Task list updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task list updated.");
return res.data;
}),
[request, taskList]
@@ -181,12 +166,7 @@ export const StaffTaskProvider = ({ children }) => {
const res = await api.post(`${BASE}/task-lists/${taskListId}/archive`);
setTaskLists((prev) => prev.filter((tl) => tl.task_list_id !== taskListId));
if (taskList?.task_list_id === taskListId) setTaskList(null);
toast("Task list archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task list archived.");
return res.data;
}),
[request, taskList]
@@ -198,12 +178,7 @@ export const StaffTaskProvider = ({ children }) => {
request(async () => {
const res = await api.post(`${BASE}/task-lists/${taskListId}/restore`);
setTaskLists((prev) => prev.filter((tl) => tl.task_list_id !== taskListId));
toast("Task list restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task list restored.");
return res.data;
}),
[request]
@@ -218,12 +193,7 @@ export const StaffTaskProvider = ({ children }) => {
setTaskLists((prev) =>
prev.filter((tl) => !archived_ids.includes(tl.task_list_id))
);
toast(`${archived_ids.length} task list(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${archived_ids.length} task list(s) archived.`);
return res.data;
}),
[request]
@@ -238,12 +208,7 @@ export const StaffTaskProvider = ({ children }) => {
setTaskLists((prev) =>
prev.filter((tl) => !restored_ids.includes(tl.task_list_id))
);
toast(`${restored_ids.length} task list(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${restored_ids.length} task list(s) restored.`);
return res.data;
}),
[request]
@@ -325,12 +290,7 @@ export const StaffTaskProvider = ({ children }) => {
)
);
}
toast("Task created.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task created.");
return res.data;
}),
[request]
@@ -360,12 +320,7 @@ export const StaffTaskProvider = ({ children }) => {
);
if (task?.task_id === taskId) setTask((prev) => ({ ...prev, ...updated }));
}
toast("Task updated.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task updated.");
return res.data;
}),
[request, task]
@@ -384,12 +339,7 @@ export const StaffTaskProvider = ({ children }) => {
: tl
)
);
toast("Task archived.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task archived.");
return res.data;
}),
[request]
@@ -401,12 +351,7 @@ export const StaffTaskProvider = ({ children }) => {
request(async () => {
const res = await api.post(`${BASE}/task-lists/${taskListId}/tasks/${taskId}/restore`);
setTasks((prev) => prev.filter((t) => t.task_id !== taskId));
toast("Task restored.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Task restored.");
return res.data;
}),
[request]
@@ -419,12 +364,7 @@ export const StaffTaskProvider = ({ children }) => {
const res = await api.post(`${BASE}/task-lists/${taskListId}/tasks/bulk-archive`, { ids });
const { archived_ids = [] } = res.data?.data ?? {};
setTasks((prev) => prev.filter((t) => !archived_ids.includes(t.task_id)));
toast(`${archived_ids.length} task(s) archived.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${archived_ids.length} task(s) archived.`);
return res.data;
}),
[request]
@@ -437,12 +377,7 @@ export const StaffTaskProvider = ({ children }) => {
const res = await api.post(`${BASE}/task-lists/${taskListId}/tasks/bulk-restore`, { ids });
const { restored_ids = [] } = res.data?.data ?? {};
setTasks((prev) => prev.filter((t) => !restored_ids.includes(t.task_id)));
toast(`${restored_ids.length} task(s) restored.`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`${restored_ids.length} task(s) restored.`);
return res.data;
}),
[request]
+1 -6
View File
@@ -37,12 +37,7 @@ export const StaffUserProvider = ({ children }) => {
} catch (err) {
const message = err?.response?.data?.message || err.message || "Something went wrong.";
setError(message);
toast(message, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(message);
return null;
} finally {
setLoading(false);
+6
View File
@@ -239,6 +239,12 @@
body {
@apply bg-background text-foreground;
}
/* Suppress Edge/IE's native password reveal & clear icons so they don't overlap our custom eye toggle */
input[type="password"]::-ms-reveal,
input[type="password"]::-ms-clear {
display: none;
}
}
/* Admin route: hardcoded violet accent for dark mode (temporary, reuses existing .dark violet tokens) */
@@ -248,12 +248,7 @@ export default function CourseAssessment() {
setLocalAssessment(result);
} catch (err) {
if (err?.response?.status !== 404) {
toast(err?.response?.data?.message ?? "Could not load assessment.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load assessment.");
}
} finally {
setInitializing(false);
@@ -290,7 +290,7 @@ export default function EditCourse() {
const handleSaveCategories = async () => {
setCategoriesLoading(true);
await syncCourseCategories(courseId, selectedCategoryIds.map(Number));
await syncCourseCategories(courseId, selectedCategoryIds);
setCategoriesDirty(false);
setCategoriesLoading(false);
};
@@ -355,12 +355,7 @@ export default function ViewAssessment() {
setLocalAssessment(data?.data?.data ?? null);
} catch (err) {
if (err?.response?.status !== 404) {
toast(err?.response?.data?.message ?? "Could not load assessment.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load assessment.");
}
}
})();
@@ -240,12 +240,7 @@ export default function ModifyQuiz() {
setLocalQuiz(result);
} catch (err) {
if (err?.response?.status !== 404) {
toast(err?.response?.data?.message ?? "Could not load quiz.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not load quiz.");
}
// 404 → no quiz yet, stay in create mode with localQuiz = null
} finally {
@@ -41,12 +41,7 @@ export default function NotificationSettings() {
const { data } = await api.get("/admin/notification-settings");
setSettings(data?.data ?? []);
} catch (err) {
toast(err?.response?.data?.message ?? "Failed to load notification settings.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Failed to load notification settings.");
} finally {
setLoading(false);
}
@@ -64,21 +59,10 @@ export default function NotificationSettings() {
const updated = data?.data?.data;
setSettings((prev) => prev.map((s) => (s.job_name === jobName ? { ...s, ...updated } : s)));
toast(
`${JOB_LABELS[jobName]?.label ?? jobName} ${enabled ? "enabled" : "disabled"}.`,
{
action: {
label: "Close",
onClick: () => {}
}
}
`${JOB_LABELS[jobName]?.label ?? jobName} ${enabled ? "enabled" : "disabled"}.`
);
} catch (err) {
toast(err?.response?.data?.message ?? "Failed to update setting.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Failed to update setting.");
} finally {
setSavingJob(null);
}
@@ -93,19 +77,9 @@ export default function NotificationSettings() {
});
const updated = data?.data?.data;
setSettings((prev) => prev.map((s) => (s.job_name === jobName ? { ...s, ...updated, preset } : s)));
toast("Schedule updated — took effect immediately, no restart needed.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Schedule updated — took effect immediately, no restart needed.");
} catch (err) {
toast(err?.response?.data?.message ?? "Failed to update schedule.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Failed to update schedule.");
} finally {
setSavingJob(null);
}
@@ -100,20 +100,10 @@ export default function PaymentPolicy() {
},
promo_rules: promoRules,
});
toast("Payment policy saved.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Payment policy saved.");
navigate("/admin/tiers/plans");
} catch (err) {
toast(err?.response?.data?.message ?? "Could not save payment policy.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not save payment policy.");
} finally {
setSaving(false);
}
@@ -123,25 +113,10 @@ export default function PaymentPolicy() {
const handleAddPromo = () => {
const code = addForm.code.trim().toUpperCase();
if (!code) { toast("Code is required.", {
action: {
label: "Close",
onClick: () => {}
}
}); return; }
if (!addForm.value || Number(addForm.value) <= 0) { toast("Value must be greater than 0.", {
action: {
label: "Close",
onClick: () => {}
}
}); return; }
if (!code) { toast("Code is required."); return; }
if (!addForm.value || Number(addForm.value) <= 0) { toast("Value must be greater than 0."); return; }
if (promoRules.some((r) => r.code.toUpperCase() === code)) {
toast("A rule with this code already exists.", {
action: {
label: "Close",
onClick: () => {}
}
}); return;
toast("A rule with this code already exists."); return;
}
const rule = {
+5 -30
View File
@@ -220,19 +220,9 @@ function PaymentPolicyTab({ planId, plan }) {
},
promo_rules: promoRules,
});
toast("Payment policy saved.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Payment policy saved.");
} catch (err) {
toast(err?.response?.data?.message ?? "Could not save payment policy.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not save payment policy.");
} finally {
setSaving(false);
}
@@ -240,24 +230,9 @@ function PaymentPolicyTab({ planId, plan }) {
const handleAddPromo = () => {
const code = addForm.code.trim().toUpperCase();
if (!code) { toast("Code is required.", {
action: {
label: "Close",
onClick: () => {}
}
}); return; }
if (!addForm.value || Number(addForm.value) <= 0) { toast("Value must be greater than 0.", {
action: {
label: "Close",
onClick: () => {}
}
}); return; }
if (promoRules.some((r) => r.code.toUpperCase() === code)) { toast("A rule with this code already exists.", {
action: {
label: "Close",
onClick: () => {}
}
}); return; }
if (!code) { toast("Code is required."); return; }
if (!addForm.value || Number(addForm.value) <= 0) { toast("Value must be greater than 0."); return; }
if (promoRules.some((r) => r.code.toUpperCase() === code)) { toast("A rule with this code already exists."); return; }
const rule = {
code,
+2 -12
View File
@@ -151,20 +151,10 @@ export default function EditUser() {
const res = await updateUser(id, payload);
if (res) {
toast("User updated successfully.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("User updated successfully.");
navigate(`../view/${id}`);
} else {
toast("Failed to update user.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Failed to update user.");
}
};
@@ -5,15 +5,19 @@
* staff, user); only reg_type matters (Google accounts are turned
* away, see LoginForm's "Please log in with Google" precedent).
* Step 1 — Email (checks reg_type server-side, sends OTP if system)
* Step 2 — OTP + new password + confirm, single submit
* Step 2 — OTP only (verified server-side before advancing)
* Step 3 — New password + confirm, submitted with the verified OTP
* Module: User Credentials
* Author: lash0000
* Date Created: Jul. 4, 2026
* Date Modified: Jul. 7, 2026 — split combined OTP+password step into its own
* verify-then-set-password steps (Kenneth Obsequio)
***********************************************************************************************************************************************************************/
import { useState, useEffect } from 'react'
import { useNavigate, Link } from 'react-router-dom'
import { useNavigate, useBlocker, Link } from 'react-router-dom'
import { useForm, Controller } from 'react-hook-form'
import { useAuth } from '@/contexts/AuthContext'
import { PageMeta } from '@/contexts/MetadataContext'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { cn } from '@/lib/utils'
@@ -25,6 +29,7 @@ import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
@@ -37,9 +42,12 @@ const emailSchema = z.object({
email: z.string().email('Invalid email address'),
})
const resetSchema = z
const otpSchema = z.object({
otp: z.string().length(6, 'Enter all 6 digits').regex(/^\d{6}$/, 'OTP must contain only digits'),
})
const passwordSchema = z
.object({
otp: z.string().length(6, 'Enter all 6 digits').regex(/^\d{6}$/, 'OTP must contain only digits'),
new_password: z
.string()
.min(8, 'Password must be at least 8 characters')
@@ -52,19 +60,27 @@ const resetSchema = z
path: ['confirm_password'],
})
const STEP_META = [
{ title: 'Forgot Password', description: 'Reset your account password.' },
{ title: 'Enter Verification Code', description: 'Enter the 6-digit code sent to your email.' },
{ title: 'Reset Password', description: 'Choose a new password for your account.' },
]
export function ForgotPasswordForm({ className, ...props }) {
const navigate = useNavigate()
const { forgotPassword, resetPassword } = useAuth()
const { forgotPassword, verifyResetOtp, resetPassword } = useAuth()
// 0 = email, 1 = otp + new password
// 0 = email, 1 = otp, 2 = new password
const [step, setStep] = useState(0)
const [pendingEmail, setPendingEmail] = useState('')
const [pendingOtp, setPendingOtp] = useState('')
const [showPassword, setShowPassword] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const [resendCooldown, setResendCooldown] = useState(0)
const [errorDialogOpen, setErrorDialogOpen] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [successDialogOpen, setSuccessDialogOpen] = useState(false)
const [googleDialogOpen, setGoogleDialogOpen] = useState(false)
useEffect(() => {
if (resendCooldown <= 0) return
@@ -72,6 +88,30 @@ export function ForgotPasswordForm({ className, ...props }) {
return () => clearTimeout(t)
}, [resendCooldown])
// Past the email step there's an in-progress code/password reset to lose —
// warn on tab close/refresh/URL change (native dialog) and on in-app back/
// forward navigation (native window.confirm via the router's blocker).
const midFlow = step > 0
useEffect(() => {
if (!midFlow) return
const handleBeforeUnload = (e) => {
e.preventDefault()
e.returnValue = ''
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => window.removeEventListener('beforeunload', handleBeforeUnload)
}, [midFlow])
const blocker = useBlocker(midFlow)
useEffect(() => {
if (blocker.state !== 'blocked') return
const leave = window.confirm('You have an unfinished password reset. Leave this page anyway?')
if (leave) blocker.proceed()
else blocker.reset()
}, [blocker])
// ── Step 1: Email ──────────────────────────────────────────────────────────
const {
register: regEmail,
@@ -86,8 +126,12 @@ export function ForgotPasswordForm({ className, ...props }) {
const result = await forgotPassword({ email })
if (!result.success) {
setErrorMessage(result.message)
setErrorDialogOpen(true)
if (result.isGoogleAccount) {
setGoogleDialogOpen(true)
} else {
setErrorMessage(result.message)
setErrorDialogOpen(true)
}
return
}
@@ -96,19 +140,22 @@ export function ForgotPasswordForm({ className, ...props }) {
setStep(1)
}
// ── Step 2: OTP + new password ────────────────────────────────────────────
const handleGoogleSignIn = () => {
window.location.href = '/api/auth/google'
}
// ── Step 2: OTP ─────────────────────────────────────────────────────────────
const {
control: resetControl,
register: regReset,
handleSubmit: submitReset,
formState: { errors: errReset, isSubmitting: isResetting },
control: otpControl,
handleSubmit: submitOtp,
formState: { errors: errOtp, isSubmitting: isVerifying },
} = useForm({
resolver: zodResolver(resetSchema),
defaultValues: { otp: '', new_password: '', confirm_password: '' },
resolver: zodResolver(otpSchema),
defaultValues: { otp: '' },
})
const onResetSubmit = async ({ otp, new_password }) => {
const result = await resetPassword({ email: pendingEmail, otp, new_password })
const onOtpSubmit = async ({ otp }) => {
const result = await verifyResetOtp({ email: pendingEmail, otp })
if (!result.success) {
setErrorMessage(result.message)
@@ -116,7 +163,8 @@ export function ForgotPasswordForm({ className, ...props }) {
return
}
setSuccessDialogOpen(true)
setPendingOtp(otp)
setStep(2)
}
const handleResend = async () => {
@@ -133,8 +181,38 @@ export function ForgotPasswordForm({ className, ...props }) {
setResendCooldown(30)
}
// ── Step 3: New password ────────────────────────────────────────────────────
const {
register: regPassword,
handleSubmit: submitPassword,
formState: { errors: errPassword, isSubmitting: isResetting },
} = useForm({
resolver: zodResolver(passwordSchema),
defaultValues: { new_password: '', confirm_password: '' },
})
const onPasswordSubmit = async ({ new_password }) => {
const result = await resetPassword({ email: pendingEmail, otp: pendingOtp, new_password })
if (!result.success) {
setErrorMessage(result.message)
setErrorDialogOpen(true)
// The verified code may have expired/rotated by now — send them back to re-verify.
setStep(1)
return
}
setSuccessDialogOpen(true)
}
return (
<>
<PageMeta
title={`${STEP_META[step].title} - Philproperties`}
description={STEP_META[step].description}
keywords="forgot password, reset password, verification code, philproperties"
/>
<div className={cn('flex flex-col', className)} {...props}>
{/* ── Step 1: Email ── */}
{step === 0 && (
@@ -183,22 +261,21 @@ export function ForgotPasswordForm({ className, ...props }) {
</form>
)}
{/* ── Step 2: OTP + new password ── */}
{/* ── Step 2: OTP ── */}
{step === 1 && (
<form onSubmit={submitReset(onResetSubmit)} className="flex flex-col gap-5">
<form onSubmit={submitOtp(onOtpSubmit)} className="flex flex-col gap-5">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold tracking-tighter">Reset your password.</h1>
<h1 className="text-2xl font-bold tracking-tighter">Enter verification code.</h1>
<p className="text-muted-foreground text-sm text-balance">
We sent a 6-digit code to{' '}
<span className="font-medium text-foreground">{pendingEmail}</span>.
Enter it below along with your new password.
</p>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-sm font-medium text-center">Verification code</label>
<Controller
control={resetControl}
control={otpControl}
name="otp"
render={({ field }) => (
<InputOTP
@@ -215,8 +292,8 @@ export function ForgotPasswordForm({ className, ...props }) {
</InputOTP>
)}
/>
{errReset.otp && (
<p className="text-xs text-destructive text-center">{errReset.otp.message}</p>
{errOtp.otp && (
<p className="text-xs text-destructive text-center">{errOtp.otp.message}</p>
)}
</div>
@@ -236,6 +313,40 @@ export function ForgotPasswordForm({ className, ...props }) {
</Button>
</div>
<Button type="submit" className="w-full" disabled={isVerifying}>
{isVerifying ? (
<span className="flex items-center gap-2">
<LoaderCircle className="size-4 animate-spin" />
Verifying...
</span>
) : (
'Verify'
)}
</Button>
<Separator />
<Button
type="button"
variant="ghost"
className="w-full text-muted-foreground"
onClick={() => setStep(0)}
>
← Back
</Button>
</form>
)}
{/* ── Step 3: New password ── */}
{step === 2 && (
<form onSubmit={submitPassword(onPasswordSubmit)} className="flex flex-col gap-5">
<div className="flex flex-col gap-1">
<h1 className="text-2xl font-bold tracking-tighter">Choose a new password.</h1>
<p className="text-muted-foreground text-sm text-balance">
Your code has been verified. Set a new password for your account.
</p>
</div>
{/* New password */}
<div className="flex flex-col gap-1.5">
<label htmlFor="new_password" className="text-sm font-medium">New password</label>
@@ -247,7 +358,7 @@ export function ForgotPasswordForm({ className, ...props }) {
autoComplete="new-password"
disabled={isResetting}
className="pr-10"
{...regReset('new_password')}
{...regPassword('new_password')}
/>
<Button
type="button"
@@ -259,8 +370,8 @@ export function ForgotPasswordForm({ className, ...props }) {
{showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</Button>
</div>
{errReset.new_password && (
<p className="text-xs text-destructive">{errReset.new_password.message}</p>
{errPassword.new_password && (
<p className="text-xs text-destructive">{errPassword.new_password.message}</p>
)}
</div>
@@ -275,7 +386,7 @@ export function ForgotPasswordForm({ className, ...props }) {
autoComplete="new-password"
disabled={isResetting}
className="pr-10"
{...regReset('confirm_password')}
{...regPassword('confirm_password')}
/>
<Button
type="button"
@@ -287,8 +398,8 @@ export function ForgotPasswordForm({ className, ...props }) {
{showConfirm ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
</Button>
</div>
{errReset.confirm_password && (
<p className="text-xs text-destructive">{errReset.confirm_password.message}</p>
{errPassword.confirm_password && (
<p className="text-xs text-destructive">{errPassword.confirm_password.message}</p>
)}
</div>
@@ -309,7 +420,7 @@ export function ForgotPasswordForm({ className, ...props }) {
type="button"
variant="ghost"
className="w-full text-muted-foreground"
onClick={() => setStep(0)}
onClick={() => setStep(1)}
>
← Back
</Button>
@@ -330,6 +441,25 @@ export function ForgotPasswordForm({ className, ...props }) {
</AlertDialogContent>
</AlertDialog>
{/* Google Account Dialog */}
<AlertDialog open={googleDialogOpen} onOpenChange={setGoogleDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>This account signs in with Google</AlertDialogTitle>
<AlertDialogDescription>
We detected that this email is linked to an account created through Google Sign-In.
Accounts created this way don't have a password stored with us, so there's nothing
to reset here — sign in with Google instead to access your account. If you'd like a
password-based login too, you can set one from your account settings after signing in.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setGoogleDialogOpen(false)}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleGoogleSignIn}>Sign in with Google</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Success Dialog */}
<AlertDialog open={successDialogOpen} onOpenChange={setSuccessDialogOpen}>
<AlertDialogContent>
+2 -12
View File
@@ -94,12 +94,7 @@ export default function ChangePassword() {
// Update local user state so must_change_password is cleared
setUser((prev) => ({ ...prev, must_change_password: false }));
toast('Password changed successfully. Welcome!', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Password changed successfully. Welcome!');
// Redirect to the correct dashboard
switch (user?.acc_type) {
@@ -109,12 +104,7 @@ export default function ChangePassword() {
default: navigate('/');
}
} catch (err) {
toast(err?.response?.data?.message || 'Could not change password.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message || 'Could not change password.');
}
};
+1 -7
View File
@@ -121,13 +121,7 @@ export default function IntroPage() {
navigate('/dashboard')
} catch (err) {
toast(
err?.response?.data?.message ?? 'Could not save your info. Please try again.',
{
action: {
label: "Close",
onClick: () => {}
}
}
err?.response?.data?.message ?? 'Could not save your info. Please try again.'
)
} finally {
setLoading(false)
+53 -45
View File
@@ -3,40 +3,38 @@
* Type of Program: Frontend Page
* Description: Landing page after the backend completes Google OIDC.
*
* OTP path → backend confirmed the Google identity but, like every other
* login path, still gates on an OTP before issuing tokens. It
* redirects here with ?otpRequired=true&email=<email> and has
* NOT set a refresh cookie yet. This page renders the shared
* OtpVerifyForm; once verified, AuthContext has user/tokens
* set and we navigate to the account's home route ourselves.
* The backend redirects here with NO query params — the outcome (otpRequired/
* email, ban details, or an error code) never touches the URL. Instead, this
* page fetches it from GET /auth/google/result, a single-use endpoint that
* reads a short-lived signed cookie the callback controller set.
*
* Trusted path → this device already cleared an OTP recently and its trust
* window is still valid. Backend redirects with
* ?otpRequired=false, having already set the refresh cookie —
* this page calls restoreSession() to pull the access token
* from it, then navigates to the account's home route.
* OTP path → result = { otpRequired: true, email }. No refresh cookie was
* set yet. Renders the shared OtpVerifyForm; once verified,
* AuthContext has user/tokens set and we navigate to the
* account's home route ourselves.
*
* Error path → backend could not complete OIDC (state mismatch, token exchange
* failure, deactivated account, etc.). It redirected here with
* ?error=<code>. No refresh cookie was set, so restoreSession()
* will fail and the user stays on this page to see the error.
* Trusted path → result = {} (nothing pending) — this device already cleared
* an OTP recently, so the backend set the real refresh cookie
* directly. This page calls restoreSession() to pull the
* access token from it, then navigates to the account's home
* route.
*
* Fallback → neither param present (shouldn't normally happen now that the
* backend always redirects with one or the other) — falls back
* to the old spinner + restoreSession()/PublicRoute behavior.
* Error path → result = { error: <code>, ... }. No refresh cookie was set,
* so the user stays on this page to see the error.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jun. 21, 2026
* Date Modified: Jul. 5, 2026 — trusted-device OTP skip path
* Date Modified: Jul. 6, 2026 — outcome moved from URL params to /auth/google/result
***********************************************************************************************************************************************************************/
import { useEffect } from 'react'
import { useSearchParams, Link, useNavigate } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { LoaderCircle, ShieldBan, UserX, AlertTriangle, RefreshCw, Clock } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { useAuth } from '@/contexts/AuthContext'
import { useDateFormat } from '@/hooks/useDateFormat'
import { getRoleHomePath } from '@/utils/roleRedirect.util'
import { OtpVerifyForm } from '@/modules/auth/components/OtpVerifyForm'
import api from '@/utils/api.util'
const ERROR_MAP = {
access_denied: { icon: UserX, message: 'You cancelled the Google sign-in.' },
@@ -47,33 +45,43 @@ const ERROR_MAP = {
}
export default function OAuthCallback() {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const { restoreSession } = useAuth()
const { fmtDateTime } = useDateFormat()
const error = searchParams.get('error')
const otpRequiredParam = searchParams.get('otpRequired')
const otpRequired = otpRequiredParam === 'true'
const trusted = otpRequiredParam === 'false'
const email = searchParams.get('email')
// null = still resolving; otherwise the parsed /auth/google/result payload.
const [result, setResult] = useState(null)
useEffect(() => {
if (!trusted) return
restoreSession().then(({ success, user }) => {
navigate(success ? getRoleHomePath(user) : '/login', { replace: true })
})
}, [trusted])
let cancelled = false
if (trusted) {
return (
<div className="flex min-h-svh items-center justify-center">
<div className="flex flex-col items-center gap-3">
<LoaderCircle className="size-6 animate-spin text-muted-foreground" />
<p className="text-sm text-muted-foreground">Signing you in...</p>
</div>
</div>
)
}
;(async () => {
let pending = {}
try {
const { data } = await api.get('/auth/google/result')
pending = data.data ?? {}
} catch {
// network hiccup — fall through to the trusted-device path below
}
if (pending.otpRequired || pending.error) {
if (!cancelled) setResult(pending)
return
}
// Nothing pending → trusted-device fast path. The refresh cookie was
// already set by the backend; restoreSession() just picks it up.
const { success, user } = await restoreSession()
if (cancelled) return
navigate(success ? getRoleHomePath(user) : '/login', { replace: true })
})()
return () => { cancelled = true }
}, [])
const error = result?.error
const otpRequired = !!result?.otpRequired
const email = result?.email
if (otpRequired && email) {
return (
@@ -91,9 +99,9 @@ export default function OAuthCallback() {
}
if (error === 'account_banned') {
const reason = searchParams.get('reason')
const banType = searchParams.get('ban_type')
const expiresAt = searchParams.get('expires_at')
const reason = result.reason
const banType = result.ban_type
const expiresAt = result.expires_at
const expiryText = expiresAt ? fmtDateTime(expiresAt) : null
return (
@@ -11,7 +11,7 @@
* loading {boolean} – show skeleton rows
* onRefresh {function} – called when refresh button is clicked
* onRowClick {function} – (taskList) => void, navigates to task list view
* statusLabel {string} – 'ongoing' | 'done' | 'overdue' — drives badge style
* statusLabel {string} – 'ongoing' | 'completed' | 'overdue' — drives badge style
***********************************************************************************************************************************************************************/
import { useState, useMemo } from 'react';
import { Input } from '@/components/ui/input';
@@ -35,9 +35,9 @@ const PAGE_SIZES = [50, 100, 1000];
// ─── Status badge per bucket ───────────────────────────────────────────────────
const StatusBadge = ({ status }) => {
const map = {
ongoing: { label: 'Ongoing', icon: Circle, className: 'bg-muted text-muted-foreground' },
done: { label: 'Done', icon: Check, className: 'bg-green-500/10 text-green-700 dark:text-green-400' },
overdue: { label: 'Overdue', icon: AlertTriangle, className: 'bg-destructive/10 text-destructive' },
ongoing: { label: 'Ongoing', icon: Circle, className: 'bg-muted text-muted-foreground' },
completed: { label: 'Completed', icon: Check, className: 'bg-green-500/10 text-green-700 dark:text-green-400' },
overdue: { label: 'Overdue', icon: AlertTriangle, className: 'bg-destructive/10 text-destructive' },
};
const { label, icon: Icon, className } = map[status] ?? map.ongoing;
return (
@@ -180,13 +180,7 @@ const FileUpload = ({
if (rejected.length > 0) {
setTimeout(() => toast(
`${rejected.length === 1 ? `"${rejected[0]}" is` : `${rejected.length} files are`} not allowed. ` +
`Accepted types: ${allowed.join(", ")}.`,
{
action: {
label: "Close",
onClick: () => {}
}
}
`Accepted types: ${allowed.join(", ")}.`
), 0);
}
}
@@ -197,25 +191,13 @@ const FileUpload = ({
const availableSlots = maxFileCount - prev.length;
if (availableSlots <= 0) {
setTimeout(() => toast(
`You can only attach up to ${maxFileCount} file${maxFileCount !== 1 ? "s" : ""}.`,
{
action: {
label: "Close",
onClick: () => {}
}
}
`You can only attach up to ${maxFileCount} file${maxFileCount !== 1 ? "s" : ""}.`
), 0);
incoming = [];
} else if (incoming.length > availableSlots) {
setTimeout(() => toast(
`Only ${availableSlots} more file${availableSlots !== 1 ? "s" : ""} can be added ` +
`(max ${maxFileCount}).`,
{
action: {
label: "Close",
onClick: () => {}
}
}
`(max ${maxFileCount}).`
), 0);
incoming = incoming.slice(0, availableSlots);
}
@@ -242,12 +224,7 @@ const FileUpload = ({
if (duplicates.length > 0) {
setTimeout(() => toast(duplicates.length === 1
? `"${duplicates[0]}" is already attached.`
: `${duplicates.length} files are already attached.`, {
action: {
label: "Close",
onClick: () => {}
}
}), 0);
: `${duplicates.length} files are already attached.`), 0);
}
const next = prev.concat(toAdd);
toAdd.forEach((e) => simulateUpload(e.id));
@@ -40,12 +40,7 @@ const ReadCourse = ({ title = "Read Course", courses = [], groupId, taskListId,
courses.forEach((course) => {
const prev = prevCompletedRef.current[course.id];
if (course.completed && prev === false) {
toast(`"${course.title}" has been automatically turned in!`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`"${course.title}" has been automatically turned in!`);
}
prevCompletedRef.current[course.id] = !!course.completed;
});
@@ -96,7 +96,7 @@ const LinkCard = ({ link, visited, onTurnIn, onUnvisit, submitting, unsubmitting
footer={
<>
<Button variant="outline" onClick={() => setModalOpen(false)}>Cancel</Button>
<Button onClick={handleTurnIn} disabled={submitting}>
<Button onClick={handleTurnIn} disabled={submitting}>
<SendHorizonal /> {submitting ? "Submitting…" : "Turn In"}
</Button>
</>
@@ -1,6 +1,5 @@
import { Outlet, useMatches, useNavigate } from "react-router-dom"
import { ThemeSwitcher } from "../components/ThemeSwitcher"
import { useTheme } from "@/contexts/ThemeContext"
import {
DropdownMenu,
DropdownMenuContent,
@@ -142,7 +141,6 @@ function getInitials(name = "") {
function ClientNav() {
const navigate = useNavigate()
const { user, logout } = useAuth()
const { setTheme } = useTheme()
// Background fetches only — nav rendering never waits on these
const { achievements, getAchievements } = useProfile()
@@ -214,7 +212,6 @@ function ClientNav() {
const handleLogout = async () => {
await logout()
setTheme('light')
navigate("/login")
}
+7 -39
View File
@@ -63,21 +63,11 @@ function SecuritySection({ user, logout }) {
const handleSubmit = async (e) => {
e.preventDefault();
if (form.new_password !== form.confirm) {
toast("New passwords do not match.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("New passwords do not match.");
return;
}
if (form.new_password.length < 8) {
toast("New password must be at least 8 characters.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("New password must be at least 8 characters.");
return;
}
setLoading(true);
@@ -86,23 +76,13 @@ function SecuritySection({ user, logout }) {
current_password: form.current_password,
new_password: form.new_password,
});
toast("Password changed. Logging you out…", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Password changed. Logging you out…");
setTimeout(async () => {
await logout();
navigate("/login");
}, 1500);
} catch (err) {
toast(err?.response?.data?.message ?? "Could not change password.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Could not change password.");
} finally {
setLoading(false);
}
@@ -288,11 +268,7 @@ function AdvertisementsSection() {
const result = await updateProfile({ [key]: value });
if (result?.success) {
toast("Preference saved.", {
description: "Reload the page for this to take effect.",
action: {
label: "Close",
onClick: () => {}
}
description: "Reload the page for this to take effect."
});
}
};
@@ -302,11 +278,7 @@ function AdvertisementsSection() {
const result = await updateProfile({ show_popup_ads: false, show_other_ads: false });
if (result?.success) {
toast("Preference saved.", {
description: "Reload the page for this to take effect.",
action: {
label: "Close",
onClick: () => {}
}
description: "Reload the page for this to take effect."
});
}
};
@@ -315,11 +287,7 @@ function AdvertisementsSection() {
const result = await updateProfile({ show_popup_ads: !hide, show_other_ads: !hide });
if (result?.success) {
toast("Preference saved.", {
description: "Reload the page for this to take effect.",
action: {
label: "Close",
onClick: () => {}
}
description: "Reload the page for this to take effect."
});
}
};
+4 -24
View File
@@ -122,12 +122,7 @@ const Checkout = () => {
if (!wasCancelled) return;
const orderId = searchParams.get("token");
if (orderId) cancelOrder(orderId);
toast("PayPal checkout was cancelled.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("PayPal checkout was cancelled.");
navigate(`/plans/checkout?plan_id=${planId}`, { replace: true });
}, [wasCancelled]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -158,19 +153,9 @@ const Checkout = () => {
const result = await validatePromo(plan.plan_id, promoCode.trim().toUpperCase());
if (result?.valid) {
setPromoResult(result);
toast("Promo code applied.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Promo code applied.");
} else {
toast(result?.reason ?? "Invalid promo code.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(result?.reason ?? "Invalid promo code.");
}
};
@@ -186,12 +171,7 @@ const Checkout = () => {
);
if (!order) return;
const approvalUrl = order.approval_url;
if (!approvalUrl) { toast("Could not get PayPal approval URL.", {
action: {
label: "Close",
onClick: () => {}
}
}); return; }
if (!approvalUrl) { toast("Could not get PayPal approval URL."); return; }
window.location.href = approvalUrl;
};
+2 -12
View File
@@ -68,12 +68,7 @@ export default function CourseCheckout() {
if (!wasCancelled) return;
const orderId = searchParams.get("token");
if (orderId) cancelCourseOrder(orderId);
toast("Payment was cancelled.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Payment was cancelled.");
navigate(`/course/${courseId}/checkout`, { replace: true });
}, [wasCancelled]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -81,12 +76,7 @@ export default function CourseCheckout() {
if (!course?.product?.id) return;
const order = await createCourseOrder(course.product.id);
if (!order) return;
if (!order.approval_url) { toast("Could not get PayPal approval URL.", {
action: {
label: "Close",
onClick: () => {}
}
}); return; }
if (!order.approval_url) { toast("Could not get PayPal approval URL."); return; }
window.location.href = order.approval_url;
};
+41 -32
View File
@@ -30,6 +30,7 @@ import { resolveTierBadge } from "@/utils/tierBadge.util";
import { useClientAdvertisements } from "@/contexts/ClientAdvertisementContext";
import { Banner, BannerSkeleton } from "@/components/generic/Blocks/Client/Advertisements/Banner";
import { Sidebar, SidebarSkeleton } from "@/components/generic/Blocks/Client/Advertisements/Sidebar";
import { Tags } from "lucide-react";
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -44,8 +45,6 @@ function formatDuration(seconds = 0) {
// ─── Spine / card helpers ──────────────────────────────────────────────────────
const INTRO_HEIGHT = 50;
const useVisibleNodes = (refs, count) => {
const [visible, setVisible] = useState(new Set());
useEffect(() => {
@@ -131,15 +130,15 @@ const UnitAccordionBlock = ({ unit, unitIndex, i, cardRefs, courseId, onToggle,
className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-slate-200 dark:hover:bg-blue-500 transition-colors cursor-pointer"
onClick={() => navigate(`/course/${courseId}/unit`, { state: { lessonId: lesson.lesson_id, unitId: unit.unit_id } })}
>
<div className="flex items-center gap-3 select-none">
<div className="flex items-center gap-3 select-none min-w-0">
{isCompleted(lesson.uuid)
? <CheckCircle2 className="size-4 text-emerald-500 shrink-0" />
: <div className="w-4 h-4 rounded-full border border-muted-foreground/40 flex items-center justify-center flex-shrink-0" />
}
<span className="text-md text-card-foreground">{lesson.title}</span>
<span className="text-md text-card-foreground truncate">{lesson.title}</span>
</div>
{lesson.duration_seconds > 0 && (
<span className="text-sm">{formatDuration(lesson.duration_seconds)}</span>
<span className="text-sm shrink-0 ml-2">{formatDuration(lesson.duration_seconds)}</span>
)}
</div>
))}
@@ -150,17 +149,19 @@ const UnitAccordionBlock = ({ unit, unitIndex, i, cardRefs, courseId, onToggle,
className="flex items-center justify-between py-2 px-3 mt-1 rounded-lg border border-dashed border-blue-300 dark:border-blue-700 bg-blue-50/60 dark:bg-blue-950/30 hover:bg-blue-100 dark:hover:bg-blue-900/40 transition-colors cursor-pointer"
onClick={() => navigate(`/course/${courseId}/unit`, { state: { quizUnitId: unit.unit_id } })}
>
<div className="flex items-center gap-3 select-none">
<div className="flex items-center gap-3 select-none min-w-0">
{quiz.has_passed
? <CheckCircle2 className="size-4 text-emerald-500 shrink-0" />
: <FileQuestion className="size-4 text-blue-500 shrink-0" />
}
<span className="text-sm font-medium text-blue-700 dark:text-blue-300">{quiz.title}</span>
<span className="text-sm font-medium text-blue-700 dark:text-blue-300 truncate">{quiz.title}</span>
</div>
<Badge className={quiz.has_passed
? "bg-green-100 text-green-700 border border-green-400 dark:bg-green-900/40 dark:text-green-400 dark:border-green-700 text-[10px]"
: "bg-blue-100 text-blue-700 border border-blue-300 dark:bg-blue-900/40 dark:text-blue-400 dark:border-blue-700 text-[10px]"
}>
<Badge className={cn(
"shrink-0 ml-2 text-[10px]",
quiz.has_passed
? "bg-green-100 text-green-700 border border-green-400 dark:bg-green-900/40 dark:text-green-400 dark:border-green-700"
: "bg-blue-100 text-blue-700 border border-blue-300 dark:bg-blue-900/40 dark:text-blue-400 dark:border-blue-700"
)}>
{quiz.has_passed ? "Passed" : "Quiz"}
</Badge>
</div>
@@ -246,7 +247,7 @@ const CertCard = ({ courseTitle, courseLevel, badgeColor, badgeImageUrl, pending
<Dialog>
<motion.div
ref={nodeRef}
className="w-[320px] rounded-2xl border bg-card p-6 flex flex-col items-center gap-4 shadow-sm"
className="w-full max-w-[320px] rounded-2xl border bg-card p-6 flex flex-col items-center gap-4 shadow-sm"
initial={{ opacity: 0, y: 14 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay, ease: "easeOut" }}
@@ -391,9 +392,9 @@ const CourseUnits = ({ units = [], courseId, courseTitle, courseLevel, badgeColo
const isIssued = !!certificate;
return (
<div ref={wrapRef} className="flex gap-5 px-4">
<div ref={wrapRef} className="flex gap-0 lg:gap-5 lg:px-4">
{/* Spine */}
<div className="relative flex-shrink-0 w-7" style={{ height: svgH }}>
<div className="relative flex-shrink-0 w-0 lg:w-7" style={{ height: svgH }}>
{mids.length > 0 && (
<>
<svg
@@ -450,7 +451,7 @@ const CourseUnits = ({ units = [], courseId, courseTitle, courseLevel, badgeColo
</div>
{/* Cards */}
<div className="xs:-ml-12 lg:-ml-0 flex flex-col gap-6 flex-1 max-w-3xl" style={{ paddingTop: INTRO_HEIGHT }}>
<div className="flex flex-col gap-6 flex-1 max-w-3xl min-w-0 xs:pt-0 lg:pt-[50px]">
{nodes.map((node, ni) => {
const delay = ni * 0.05;
const nodeRef = (el) => (cardRefs.current[ni] = el);
@@ -566,12 +567,7 @@ const CourseDetails = () => {
}, [course?.badge_asset_id, course?.badge_image_url]);
if (courseBlocked) {
toast("You don't have access to this course. Upgrade your plan.", {
action: {
label: "Close",
onClick: () => { }
}
});
toast("You don't have access to this course. Upgrade your plan.");
navigate("/course", { replace: true });
return null;
}
@@ -616,12 +612,20 @@ const CourseDetails = () => {
<Hourglass className="size-3" /> Coming Soon
</Badge>
) : (
<Button onClick={() => navigate(`/course/${courseId}/unit`)}>
{hasCompleted
? <><CheckCheck /> Start Again</>
: <><SendHorizonal /> Start Learning</>
}
</Button>
<>
<Button size="sm" className="lg:hidden" onClick={() => navigate(`/course/${courseId}/unit`)}>
{hasCompleted
? <><CheckCheck /> Start Again</>
: <><SendHorizonal /> Start Learning</>
}
</Button>
<Button size="default" className="hidden lg:inline-flex" onClick={() => navigate(`/course/${courseId}/unit`)}>
{hasCompleted
? <><CheckCheck /> Start Again</>
: <><SendHorizonal /> Start Learning</>
}
</Button>
</>
)}
</div>
</div>
@@ -648,15 +652,20 @@ const CourseDetails = () => {
</div>
<div className="flex lg:flex-row items-start justify-between w-full text-white">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
{(() => {
const slug = course?.plan_tier ?? course?.subscription ?? "free";
const { label, cls } = resolveTierBadge(slug, tierMap);
return <Badge className={cls}>{label}</Badge>;
})()}
{(course?.categories ?? []).map((cat) => (
<Badge key={cat.id} className="bg-white/15 text-white border-white/30">
<Tags /> {cat.name}
</Badge>
))}
</div>
<h1 className="font-bold text-4xl">{course?.title ?? "Course Title"}</h1>
<p className="max-w-2xl lg:text-lg">{course?.description ?? ""}</p>
<h1 className="font-bold xs:text-2xl lg:text-4xl">{course?.title ?? "Course Title"}</h1>
<p className="max-w-2xl xs:text-sm lg:text-lg">{course?.description ?? ""}</p>
<div className="flex items-center gap-4">
{course?.duration_seconds > 0 && (
<div className="[&_svg]:size-4 flex gap-1.5 items-center">
@@ -704,9 +713,9 @@ const CourseDetails = () => {
</div>
{/* Body */}
<div className="lg:container lg:mx-auto flex flex-col gap-6 xs:px-6 xs:py-0 lg:py-8">
<div className="lg:container lg:mx-auto flex flex-col gap-6 xs:px-3 xs:-mt-5 lg:-mt-0">
<div className="flex flex-col lg:flex-row gap-8">
<div className="flex flex-col xs:gap-4 lg:gap-12 flex-1 min-w-0">
<div className="flex flex-col xs:gap-12 lg:gap-12 flex-1 min-w-0">
<div className="space-y-4">
<div className="font-bold text-2xl">About this course</div>
<div className="max-w-3xl space-y-4 text-muted-foreground lg:text-lg">
+41 -18
View File
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { House, Timer, ChevronLeft, ChevronRight, LockIcon, Tag, Check, ShoppingCart } from "lucide-react";
import { House, Timer, ChevronLeft, ChevronRight, LockIcon, Tag, Tags, Check, ShoppingCart } from "lucide-react";
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
import { Input } from "@/components/ui/input";
import {
@@ -20,6 +20,7 @@ import { resolveTierBadge } from "@/utils/tierBadge.util";
import { Building2 } from "lucide-react";
import { useClientAdvertisements } from "@/contexts/ClientAdvertisementContext";
import { Banner, BannerSkeleton } from "@/components/generic/Blocks/Client/Advertisements/Banner";
import { GitBranch } from "lucide-react";
// ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -37,8 +38,8 @@ const ITEMS_PER_PAGE = 10;
// ─── Course Card ──────────────────────────────────────────────────────────────
const CourseCard = ({ course, tierMap, onViewDetails }) => {
const slug = course.subscription ?? "free";
const locked = course.is_locked;
const slug = course.subscription ?? "free";
const locked = course.is_locked;
const duration = formatDuration(course.duration_seconds);
const { rank, label, cls } = resolveTierBadge(slug, tierMap);
@@ -58,11 +59,14 @@ const CourseCard = ({ course, tierMap, onViewDetails }) => {
{locked ? <LockIcon className="size-3" /> : <Tag className="size-3" />}
{label}
</Badge>
{course.level && (
<Badge variant="outline">{course.level.charAt(0).toUpperCase() + course.level.slice(1)}</Badge>
{course.level && !locked && (
<Badge variant="outline"><GitBranch />{course.level.charAt(0).toUpperCase() + course.level.slice(1)}</Badge>
)}
{(course.categories ?? []).map((cat) => (
<Badge key={cat.id} variant="outline"><Tags /> {cat.name}</Badge>
))}
{locked && (
<Badge variant="secondary" className="ml-auto">
<Badge variant="secondary">
<LockIcon className="size-3" /> Locked
</Badge>
)}
@@ -113,7 +117,7 @@ const CourseCardSkeleton = () => (
const Pagination = ({ currentPage, totalPages, totalItems, itemsPerPage, onPageChange }) => {
const start = (currentPage - 1) * itemsPerPage + 1;
const end = Math.min(currentPage * itemsPerPage, totalItems);
const end = Math.min(currentPage * itemsPerPage, totalItems);
const getPages = () => {
const pages = [];
@@ -165,12 +169,12 @@ const CoursesList = () => {
const { advertisements, loading: adLoading, getActiveAdvertisement, handleAdCtaClick } = useClientAdvertisements();
const [tierCategories, setTierCategories] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [search, setSearch] = useState("");
const [levelFilter, setLevelFilter] = useState("All");
const [subFilter, setSubFilter] = useState("All");
const [currentPage, setCurrentPage] = useState(1);
const [search, setSearch] = useState("");
const [levelFilter, setLevelFilter] = useState("All");
const [subFilter, setSubFilter] = useState("All");
const [categoryFilter, setCategoryFilter] = useState("All");
const [modalOpen, setModalOpen] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
const [selectedCourse, setSelectedCourse] = useState(null);
const [allCategories, setAllCategories] = useState([]);
@@ -179,10 +183,10 @@ const CoursesList = () => {
getCourses();
api.get("/client/tiers/categories")
.then(({ data }) => setTierCategories(data.data ?? []))
.catch(() => {});
.catch(() => { });
api.get("/client/courses/categories")
.then(({ data }) => setAllCategories(data.data ?? []))
.catch(() => {});
.catch(() => { });
getActiveAdvertisement("course_list.banner");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -199,10 +203,10 @@ const CoursesList = () => {
const filtered = useMemo(() =>
courses
.filter((c) => {
const matchSearch = c.title.toLowerCase().includes(search.toLowerCase()) ||
const matchSearch = c.title.toLowerCase().includes(search.toLowerCase()) ||
(c.description ?? "").toLowerCase().includes(search.toLowerCase());
const matchLevel = levelFilter === "All" || (c.level ?? "").toLowerCase() === levelFilter.toLowerCase();
const matchSub = subFilter === "All" || c.subscription === subFilter;
const matchLevel = levelFilter === "All" || (c.level ?? "").toLowerCase() === levelFilter.toLowerCase();
const matchSub = subFilter === "All" || c.subscription === subFilter;
const matchCategory = categoryFilter === "All" || (c.categories ?? []).some((cat) => String(cat.id) === categoryFilter);
return matchSearch && matchLevel && matchSub && matchCategory;
})
@@ -211,7 +215,7 @@ const CoursesList = () => {
);
const totalPages = Math.max(1, Math.ceil(filtered.length / ITEMS_PER_PAGE));
const paginated = filtered.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
const paginated = filtered.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE);
const handleViewDetails = (course) => {
if (course.is_locked) {
@@ -299,6 +303,25 @@ const CoursesList = () => {
<Banner ad={bannerAd} onCtaClick={handleAdCtaClick} />
)}
{categoryFilter !== "All" && (
<div className="flex items-center flex-wrap gap-2">
<span className="text-sm text-muted-foreground">Tags:</span>
{allCategories
.filter((cat) => String(cat.id) === categoryFilter)
.map((cat) => (
<Badge
key={cat.id}
variant="default"
className="cursor-pointer"
onClick={() => { setCategoryFilter("All"); setCurrentPage(1); }}
>
<Tags className="size-3" />
{cat.name}
</Badge>
))}
</div>
)}
{/* Course Grid */}
{coursesLoading ? (
<div className="xs:pt-12 lg:pt-0 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
-4
View File
@@ -229,10 +229,6 @@ const Client = () => {
toast('Welcome to Philproperties!', {
description: 'You earned the Early Access badge. Check your notifications for details.',
duration: 6000,
action: {
label: "Close",
onClick: () => {}
}
});
window.history.replaceState({}, '');
}, []);
+1 -6
View File
@@ -30,12 +30,7 @@ const CertificateCard = ({ courseTitle, issuedAt, courseUuid, badgeColor, badgeI
a.click();
URL.revokeObjectURL(url);
} catch {
toast("Could not download certificate.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Could not download certificate.");
} finally {
setDownloading(false);
}
+2 -12
View File
@@ -141,18 +141,8 @@ export default function Notifications() {
const ok = await clearAll();
setClearing(false);
setClearOpen(false);
if (ok) toast("All notifications cleared.", {
action: {
label: "Close",
onClick: () => {}
}
});
else toast("Could not clear notifications.", {
action: {
label: "Close",
onClick: () => {}
}
});
if (ok) toast("All notifications cleared.");
else toast("Could not clear notifications.");
}
const rangeStart = pagination.total === 0 ? 0 : (pagination.page - 1) * pagination.limit + 1;
+2 -12
View File
@@ -385,22 +385,12 @@ export default function PlanList() {
setRefundLoading(true);
try {
const { data } = await api.post("/client/tiers/checkout/refund");
toast(data.message ?? "Refund processed. Your access has been revoked.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(data.message ?? "Refund processed. Your access has been revoked.");
setRefundPlan(null);
resetMyTier();
getMyTier();
} catch (err) {
toast(err?.response?.data?.message ?? "Refund failed. Please try again.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast(err?.response?.data?.message ?? "Refund failed. Please try again.");
} finally {
setRefundLoading(false);
}
+9 -8
View File
@@ -134,12 +134,7 @@ const LandscapeCertCard = ({ courseTitle, issuedAt, courseUuid, badgeColor, badg
a.click();
URL.revokeObjectURL(url);
} catch {
toast("Could not download certificate.", {
action: {
label: "Close",
onClick: () => {}
}
});
toast("Could not download certificate.");
} finally {
setDownloading(false);
}
@@ -525,16 +520,22 @@ const ProfilePage = () => {
}
>
{pendingModalCourse && (() => {
const { pending_quizzes = [], pending_assessment } = pendingModalCourse;
const { pending_quizzes = [], pending_assessment, assessment_configured = true } = pendingModalCourse;
const hasQuizzes = pending_quizzes.length > 0;
const hasAssessment = !!pending_assessment;
if (!hasQuizzes && !hasAssessment) {
if (!hasQuizzes && !hasAssessment && assessment_configured) {
return (
<p className="text-sm text-muted-foreground py-2">No pending items found.</p>
);
}
return (
<div className="space-y-4 py-1">
{!assessment_configured && (
<p className="text-sm text-muted-foreground">
You've finished all lessons, but this course's final assessment hasn't been set up yet.
Check back once it's available — the course can't be marked complete until then.
</p>
)}
{hasQuizzes && (
<div>
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground mb-2">Unit Quizzes</p>
+3 -8
View File
@@ -507,12 +507,7 @@ const UnitList = () => {
useEffect(() => {
if (!completedTasks.length) return;
completedTasks.forEach((t) => {
toast(`"${t.task_name}" automatically turned in!`, {
action: {
label: "Close",
onClick: () => {}
}
});
toast(`"${t.task_name}" automatically turned in!`);
});
clearCompletedTasks();
}, [completedTasks]);
@@ -822,7 +817,7 @@ const UnitList = () => {
{/* ── Task-mode banner ─────────────────────────────────────────── */}
{taskCtx?.has_task && (
<div className={`fixed top-[112px] left-0 right-0 z-30 flex items-center gap-2 text-white text-xs font-medium px-4 py-1.5 shadow-sm transition-colors ${
<div className={`fixed xs:top-[124px] lg:top-[112px] left-0 right-0 z-30 flex items-center gap-2 text-white text-xs font-medium px-4 py-1.5 shadow-sm transition-colors ${
course?.is_completed ? 'bg-green-600' : 'bg-blue-600'
}`}>
<ListChecks className="size-3.5 shrink-0" />
@@ -976,7 +971,7 @@ const UnitList = () => {
</div>
{/* ── Main content ── */}
<div className={`${taskCtx?.has_task ? "mt-[9.5rem]" : "mt-32"} ${showSidebarContent ? "lg:ml-80" : "lg:ml-14"} p-4 md:p-6 min-h-screen`}>
<div className={`${taskCtx?.has_task ? "xs:mt-42 lg:mt-36" : "mt-32"} ${showSidebarContent ? "lg:ml-80" : "lg:ml-14"} p-4 md:p-6 min-h-screen`}>
<div className="relative w-full h-full">
{selectedCompletion ? (
<CourseCompleteBlock course={course} />
+29 -15
View File
@@ -315,12 +315,7 @@ const ViewTask = () => {
}
if (!uploadedFiles.length) {
toast('No files were uploaded successfully.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('No files were uploaded successfully.');
return;
}
@@ -334,12 +329,7 @@ const ViewTask = () => {
setNote('');
setUploadState({ files: [], isUploading: false });
} catch (err) {
toast('Failed to submit. Please try again.', {
action: {
label: "Close",
onClick: () => {}
}
});
toast('Failed to submit. Please try again.');
} finally {
setSubmitting(false);
}
@@ -413,7 +403,7 @@ const ViewTask = () => {
<div className="grid lg:grid-cols-[1fr_350px] gap-6 items-start">
{/* ── Left ──────────────────────────────────────────────── */}
<div className="flex flex-col gap-6 xs:order-1 lg:order-0 min-w-0 w-full max-w-full">
<div className="flex flex-col gap-6 min-w-0 w-full max-w-full">
{/* Task header card */}
<div className="border rounded-lg bg-card overflow-hidden">
@@ -448,6 +438,18 @@ const ViewTask = () => {
</div>
</div>
{/* Requirements status panel — mobile only, right after the header */}
{!isResolving && requirements.length > 0 && (
<div className="lg:hidden">
<RequirementsStatusPanel
requirements={requirements}
latestCompletion={latestCompletion}
isVisited={isVisited}
isCompleted={isCompleted}
/>
</div>
)}
{/* Requirements section */}
{!isResolving && requirements.length > 0 && (
<>
@@ -519,10 +521,22 @@ const ViewTask = () => {
)}
</>
)}
{/* Your work — mobile only, at the very end */}
{hasFileUpload && (
<div className="lg:hidden">
<FileUploadPanel
latestCompletion={latestCompletion}
onAddAttachment={() => setTaskModal(true)}
submitting={submitting}
onFileClick={(file) => setPreviewFile(file)}
/>
</div>
)}
</div>
{/* ── Right ─────────────────────────────────────────────── */}
<div className="flex flex-col gap-4 xs:order-1 lg:order-0 lg:sticky lg:top-24 lg:self-start select-none">
{/* ── Right (desktop sidebar only) ───────────────────────── */}
<div className="hidden lg:flex lg:flex-col gap-4 lg:sticky lg:top-24 lg:self-start select-none">
{hasFileUpload && (
<FileUploadPanel
latestCompletion={latestCompletion}
+8 -8
View File
@@ -2,7 +2,7 @@
* File Name : ViewTaskDetails.jsx
* Type : Page (Client)
* Description : Lists tasks within a task list, grouped by status tabs:
* Ongoing | Done | Overdue. Supports grid and list views.
* Ongoing | Completed | Overdue. Supports grid and list views.
* Server-side filtered via fetchTaskList(groupId, taskListId, { status }).
* Route: /group/:groupId/view/:taskListId (index)
***********************************************************************************************************************************************************************/
@@ -27,9 +27,9 @@ import ClientTaskListTable from '../components/TaskListTable';
// ─── Status tab map ───────────────────────────────────────────────────────────
const TABS = [
{ value: 'tab-ongoing', label: 'Ongoing', status: 'ongoing' },
{ value: 'tab-done', label: 'Done', status: 'done' },
{ value: 'tab-overdue', label: 'Overdue', status: 'overdue' },
{ value: 'tab-ongoing', label: 'Ongoing', status: 'ongoing' },
{ value: 'tab-completed', label: 'Completed', status: 'completed' },
{ value: 'tab-overdue', label: 'Overdue', status: 'overdue' },
];
const DAY_MS = 24 * 60 * 60 * 1000;
@@ -45,7 +45,7 @@ const TaskStatusBadge = ({ task }) => {
return (
<span className="inline-flex items-center gap-1 text-xs font-medium bg-green-500/10 text-green-700 dark:text-green-400 rounded-full px-2.5 py-1">
<Check className="size-3.5" />
Done
Completed
</span>
);
}
@@ -282,9 +282,9 @@ const ViewTaskDetails = () => {
{renderTab('No ongoing tasks right now.', 'ongoing')}
</TabsPanel>
{/* ── Done ── */}
<TabsPanel value="tab-done" className="pt-4">
{renderTab('No completed tasks yet.', 'done')}
{/* ── Completed ── */}
<TabsPanel value="tab-completed" className="pt-4">
{renderTab('No completed tasks yet.', 'completed')}
</TabsPanel>
{/* ── Overdue ── */}
+1 -6
View File
@@ -205,12 +205,7 @@ function LandingPage() {
<Button
variant="ghost"
size="icon"
onClick={() => handleCopy(value) + toast("Copied text successfully!", {
action: {
label: "Close",
onClick: () => {}
}
})}
onClick={() => handleCopy(value) + toast("Copied text successfully!")}
>
{copied === value ? (
<Check className="size-4" />