mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
testing 127.0.0.1 issue
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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={{
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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); }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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); }
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: () => {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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: () => {}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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({}, '');
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 ── */}
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user