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