add: more things

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-06 15:21:36 +08:00
parent 41e98bb602
commit 244aa607f7
71 changed files with 2998 additions and 1014 deletions
+123 -186
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { KeyRound, CreditCard, Mail, Megaphone, Info, ChevronRight, Eye, EyeOff, ShieldAlert, Trash2 } from "lucide-react";
import { KeyRound, CreditCard, Megaphone, ChevronRight, Eye, EyeOff, ShieldAlert, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
@@ -63,11 +63,21 @@ function SecuritySection({ user, logout }) {
const handleSubmit = async (e) => {
e.preventDefault();
if (form.new_password !== form.confirm) {
toast.error("New passwords do not match.");
toast("New passwords do not match.", {
action: {
label: "Close",
onClick: () => {}
}
});
return;
}
if (form.new_password.length < 8) {
toast.error("New password must be at least 8 characters.");
toast("New password must be at least 8 characters.", {
action: {
label: "Close",
onClick: () => {}
}
});
return;
}
setLoading(true);
@@ -76,13 +86,23 @@ function SecuritySection({ user, logout }) {
current_password: form.current_password,
new_password: form.new_password,
});
toast.success("Password changed. Logging you out…");
toast("Password changed. Logging you out…", {
action: {
label: "Close",
onClick: () => {}
}
});
setTimeout(async () => {
await logout();
navigate("/login");
}, 1500);
} catch (err) {
toast.error(err?.response?.data?.message ?? "Could not change password.");
toast(err?.response?.data?.message ?? "Could not change password.", {
action: {
label: "Close",
onClick: () => {}
}
});
} finally {
setLoading(false);
}
@@ -229,111 +249,6 @@ function SubscriptionSection() {
);
}
// ─── Newsletter ───────────────────────────────────────────────────────────────
const NEWSLETTER_OPTIONS = [
{
key: "newsletter_course_updates",
label: "Course updates",
description: "Emails about new courses, lesson releases, and learning milestones.",
},
{
key: "newsletter_announcements",
label: "Announcements",
description: "Platform news, promotions, and important updates from Philproperties.",
},
];
function NewsletterSection() {
const { profile, getProfile, updateProfile, profileLoading } = useProfile();
useEffect(() => {
getProfile();
}, []);
const handleToggle = async (key, value) => {
const result = await updateProfile({ [key]: value });
if (result?.success) {
toast.success(value ? "Preference saved." : "Preference saved.");
}
};
return (
<div className="space-y-4">
{NEWSLETTER_OPTIONS.map((opt, i) => (
<div key={opt.key}>
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<p className="text-sm font-medium">{opt.label}</p>
<p className="text-xs text-muted-foreground">{opt.description}</p>
</div>
{profileLoading ? (
<Skeleton className="h-6 w-11 rounded-full shrink-0" />
) : (
<Switch
checked={profile?.personal_info?.[opt.key] ?? false}
onCheckedChange={(v) => handleToggle(opt.key, v)}
/>
)}
</div>
{i < NEWSLETTER_OPTIONS.length - 1 && <Separator className="mt-4" />}
</div>
))}
</div>
);
}
// ─── Course Notices ───────────────────────────────────────────────────────────
// One-time informational dialogs shown while studying (e.g. InfoDialog in
// UnitList.jsx) — this list grows as more generic client notices are added.
const NOTICE_OPTIONS = [
{
key: "show_course_notices",
label: "Course notices",
description: "Informational pop-ups about course readiness, such as when an assessment hasn't been built yet.",
},
];
function CourseNoticesSection() {
const { profile, getProfile, updateProfile, profileLoading } = useProfile();
useEffect(() => {
getProfile();
}, []);
const handleToggle = async (key, value) => {
const result = await updateProfile({ [key]: value });
if (result?.success) {
toast.success("Preference saved.");
}
};
return (
<div className="space-y-4">
{NOTICE_OPTIONS.map((opt, i) => (
<div key={opt.key}>
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<p className="text-sm font-medium">{opt.label}</p>
<p className="text-xs text-muted-foreground">{opt.description}</p>
</div>
{profileLoading ? (
<Skeleton className="h-6 w-11 rounded-full shrink-0" />
) : (
<Switch
checked={profile?.personal_info?.[opt.key] ?? true}
onCheckedChange={(v) => handleToggle(opt.key, v)}
/>
)}
</div>
{i < NOTICE_OPTIONS.length - 1 && <Separator className="mt-4" />}
</div>
))}
</div>
);
}
// ─── Advertisements ───────────────────────────────────────────────────────────
const AD_OPTIONS = [
@@ -372,7 +287,13 @@ function AdvertisementsSection() {
}
const result = await updateProfile({ [key]: value });
if (result?.success) {
toast.success("Preference saved.", { description: "Reload the page for this to take effect." });
toast("Preference saved.", {
description: "Reload the page for this to take effect.",
action: {
label: "Close",
onClick: () => {}
}
});
}
};
@@ -380,14 +301,26 @@ function AdvertisementsSection() {
setConfirmPopupOff(false);
const result = await updateProfile({ show_popup_ads: false, show_other_ads: false });
if (result?.success) {
toast.success("Preference saved.", { description: "Reload the page for this to take effect." });
toast("Preference saved.", {
description: "Reload the page for this to take effect.",
action: {
label: "Close",
onClick: () => {}
}
});
}
};
const handleHideAllToggle = async (hide) => {
const result = await updateProfile({ show_popup_ads: !hide, show_other_ads: !hide });
if (result?.success) {
toast.success("Preference saved.", { description: "Reload the page for this to take effect." });
toast("Preference saved.", {
description: "Reload the page for this to take effect.",
action: {
label: "Close",
onClick: () => {}
}
});
}
};
@@ -454,69 +387,81 @@ function AdvertisementsSection() {
}
// ─── Delete Account ───────────────────────────────────────────────────────────
function DeleteAccountSection({ logout }) {
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const handleDelete = async () => {
setLoading(true);
try {
await api.delete("/client/profile");
toast.success("Account deleted. Goodbye!");
await logout();
navigate("/login");
} catch (err) {
toast.error(err?.response?.data?.message ?? "Could not delete account.");
setLoading(false);
}
};
return (
<>
<div className="flex items-start justify-between gap-4">
<div className="space-y-1">
<p className="text-sm font-medium text-destructive">Delete account</p>
<p className="text-xs text-muted-foreground">
Permanently remove your account and all associated data. This action cannot be undone.
</p>
</div>
<Button
variant="destructive"
size="sm"
className="shrink-0"
onClick={() => setOpen(true)}
>
<Trash2 className="size-3.5 mr-1.5" />
Delete account
</Button>
</div>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete your account?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete your account and sign you out of all sessions.
Your data cannot be recovered after deletion.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDelete}
disabled={loading}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{loading ? "Deleting…" : "Yes, delete my account"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
// Disabled while still in development — keep implemented for when we're ready
// to expose self-service account deletion.
//
// function DeleteAccountSection({ logout }) {
// const navigate = useNavigate();
// const [open, setOpen] = useState(false);
// const [loading, setLoading] = useState(false);
//
// const handleDelete = async () => {
// setLoading(true);
// try {
// await api.delete("/client/profile");
// toast("Account deleted. Goodbye!", {
// action: {
// label: "Close",
// onClick: () => {}
// }
// });
// await logout();
// navigate("/login");
// } catch (err) {
// toast(err?.response?.data?.message ?? "Could not delete account.", {
// action: {
// label: "Close",
// onClick: () => {}
// }
// });
// setLoading(false);
// }
// };
//
// return (
// <>
// <div className="flex items-start justify-between gap-4">
// <div className="space-y-1">
// <p className="text-sm font-medium text-destructive">Delete account</p>
// <p className="text-xs text-muted-foreground">
// Permanently remove your account and all associated data. This action cannot be undone.
// </p>
// </div>
// <Button
// variant="destructive"
// size="sm"
// className="shrink-0"
// onClick={() => setOpen(true)}
// >
// <Trash2 className="size-3.5 mr-1.5" />
// Delete account
// </Button>
// </div>
//
// <AlertDialog open={open} onOpenChange={setOpen}>
// <AlertDialogContent>
// <AlertDialogHeader>
// <AlertDialogTitle>Delete your account?</AlertDialogTitle>
// <AlertDialogDescription>
// This will permanently delete your account and sign you out of all sessions.
// Your data cannot be recovered after deletion.
// </AlertDialogDescription>
// </AlertDialogHeader>
// <AlertDialogFooter>
// <AlertDialogCancel disabled={loading}>Cancel</AlertDialogCancel>
// <AlertDialogAction
// onClick={handleDelete}
// disabled={loading}
// className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
// >
// {loading ? "Deleting…" : "Yes, delete my account"}
// </AlertDialogAction>
// </AlertDialogFooter>
// </AlertDialogContent>
// </AlertDialog>
// </>
// );
// }
// ─── Page ─────────────────────────────────────────────────────────────────────
@@ -539,21 +484,13 @@ export default function AccountSettings() {
<SubscriptionSection />
</Section>
<Section icon={Mail} title="Newsletter" description="Choose what emails you want to receive from us.">
<NewsletterSection />
</Section>
<Section icon={Info} title="Course Notices" description="Control informational notices shown while studying.">
<CourseNoticesSection />
</Section>
<Section icon={Megaphone} title="Advertisements" description="Control which advertisements you see across the platform.">
<AdvertisementsSection />
</Section>
<Section icon={Trash2} title="Danger Zone" description="Irreversible account actions.">
{/* <Section icon={Trash2} title="Danger Zone" description="Irreversible account actions.">
<DeleteAccountSection logout={logout} />
</Section>
</Section> */}
</div>
</div>
);