add: more commits

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-03 16:21:27 +08:00
parent 17326b2c2e
commit 7e964f2432
112 changed files with 9160 additions and 3461 deletions
+106 -60
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { KeyRound, CreditCard, Mail, ChevronRight, Eye, EyeOff, ShieldAlert, Trash2, Globe } from "lucide-react";
import { KeyRound, CreditCard, Mail, 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";
@@ -20,12 +20,10 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useAuth } from "@/contexts/AuthContext";
import { useProfile } from "@/contexts/ProfileProvider";
import { useClientTiers } from "@/contexts/ClientTiersProvider";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useCurrencyPreference } from "@/contexts/CurrencyPreferenceContext";
import api from "@/utils/api.util";
import { toast } from "sonner";
@@ -285,74 +283,122 @@ function NewsletterSection() {
);
}
// ─── Currency Preference ─────────────────────────────────────────────────────
// ─── Advertisements ───────────────────────────────────────────────────────────
function CurrencySection() {
const { profile, getProfile } = useProfile();
const { setCurrency } = useCurrencyPreference();
const [currencies, setCurrencies] = useState([]);
const [localValue, setLocalValue] = useState("USD");
const [saving, setSaving] = useState(false);
const AD_OPTIONS = [
{
key: "show_popup_ads",
label: "Popup ads",
description: "Show promotional popups when you open pages like the dashboard.",
},
{
key: "show_other_ads",
label: "Other ads",
description: "Show banner, hero, and sidebar advertisements across the site.",
},
];
function AdvertisementsSection() {
const { profile, getProfile, updateProfile, profileLoading } = useProfile();
const [confirmPopupOff, setConfirmPopupOff] = useState(false);
useEffect(() => {
getProfile();
api.get("/client/tiers/currencies")
.then(({ data }) => setCurrencies(data.data ?? []))
.catch(() => {});
}, []);
// Sync local value when profile loads or changes
useEffect(() => {
if (profile?.preferred_currency) setLocalValue(profile.preferred_currency);
}, [profile?.preferred_currency]);
const showPopupAds = profile?.personal_info?.show_popup_ads ?? true;
const showOtherAds = profile?.personal_info?.show_other_ads ?? true;
// No separate stored flag — "hidden" just means both underlying toggles are off,
// so it can never drift out of sync with them.
const hideAllAds = !showPopupAds && !showOtherAds;
const savedValue = profile?.preferred_currency ?? "USD";
const isDirty = localValue !== savedValue;
const handleToggle = async (key, value) => {
// Turning popup ads off also turns off other ads — confirm first since
// it's a bigger change than the switch being flipped suggests.
if (key === "show_popup_ads" && value === false) {
setConfirmPopupOff(true);
return;
}
const result = await updateProfile({ [key]: value });
if (result?.success) {
toast.success("Preference saved.", { description: "Reload the page for this to take effect." });
}
};
const handleSave = async () => {
setSaving(true);
try {
await api.patch("/client/profile/currency", { currency: localValue });
setCurrency(localValue);
toast.success("Currency preference saved.");
} catch (err) {
toast.error(err?.response?.data?.message ?? "Could not save preference.");
setLocalValue(savedValue); // revert on error
} finally {
setSaving(false);
const confirmTurnOffPopupAndOther = async () => {
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." });
}
};
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." });
}
};
return (
<div className="space-y-3">
<div className="space-y-1.5 max-w-xs">
<Label>Preferred currency</Label>
{!profile ? (
<Skeleton className="h-9 w-full" />
) : (
<Select value={localValue} onValueChange={setLocalValue} disabled={saving}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{currencies.map((c) => (
<SelectItem key={c.code} value={c.code}>
{c.code} — {c.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<p className="text-xs text-muted-foreground">
Plans without localized prices always display in USD regardless of this setting.
</p>
<>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between gap-4">
<div className="space-y-0.5">
<p className="text-sm font-medium">Hide all ads</p>
<p className="text-xs text-muted-foreground">
Turn off every advertisement across the platform, popups included.
</p>
</div>
{profileLoading ? (
<Skeleton className="h-6 w-11 rounded-full shrink-0" />
) : (
<Switch checked={hideAllAds} onCheckedChange={handleHideAllToggle} />
)}
</div>
<Separator className="mt-4" />
</div>
{AD_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 < AD_OPTIONS.length - 1 && <Separator className="mt-4" />}
</div>
))}
</div>
{isDirty && (
<Button size="sm" onClick={handleSave} disabled={saving}>
{saving ? "Saving…" : "Save"}
</Button>
)}
</div>
<AlertDialog open={confirmPopupOff} onOpenChange={setConfirmPopupOff}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Turn off popup ads?</AlertDialogTitle>
<AlertDialogDescription>
This will also turn off Other ads (banner, hero, and sidebar advertisements).
You can turn either back on here anytime.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmTurnOffPopupAndOther}>
Turn off both
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}
@@ -446,8 +492,8 @@ export default function AccountSettings() {
<NewsletterSection />
</Section>
<Section icon={Globe} title="Currency Preference" description="Set the currency used to display plan prices across the platform.">
<CurrencySection />
<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.">