mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user