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, ChevronRight, Eye, EyeOff, ShieldAlert, Trash2 } from "lucide-react";
|
||||
import { KeyRound, CreditCard, Mail, ChevronRight, Eye, EyeOff, ShieldAlert, Trash2, Globe } 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";
|
||||
@@ -9,6 +9,7 @@ import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -19,10 +20,12 @@ 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";
|
||||
|
||||
@@ -190,7 +193,7 @@ function SubscriptionSection() {
|
||||
) : (
|
||||
<div className="rounded-lg border overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-muted/50">
|
||||
<thead className="bg-muted/50 sticky top-0 z-10">
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground">Date</th>
|
||||
<th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground">Plan</th>
|
||||
@@ -198,25 +201,29 @@ function SubscriptionSection() {
|
||||
<th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{payments.map((p) => (
|
||||
<tr key={p.payment_id} className="hover:bg-muted/30 transition-colors">
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{fmtDate(p.createdAt)}
|
||||
</td>
|
||||
<td className="px-4 py-3 capitalize">{p.plan?.tier ?? "—"}</td>
|
||||
<td className="px-4 py-3">
|
||||
{p.currency} {fmtNumber(p.amount ?? 0)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={p.status === "completed" ? "outline" : ""} className="capitalize text-xs">
|
||||
{p.status}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<ScrollArea className="h-[192px]">
|
||||
<table className="w-full text-sm">
|
||||
<tbody className="divide-y">
|
||||
{payments.map((p) => (
|
||||
<tr key={p.payment_id} className="hover:bg-muted/30 transition-colors">
|
||||
<td className="px-4 py-3 text-muted-foreground">
|
||||
{fmtDate(p.createdAt)}
|
||||
</td>
|
||||
<td className="px-4 py-3 capitalize">{p.plan?.tier ?? "—"}</td>
|
||||
<td className="px-4 py-3">
|
||||
{p.currency} {fmtNumber(p.amount ?? 0)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge variant={p.status === "completed" ? "outline" : ""} className="capitalize text-xs">
|
||||
{p.status}
|
||||
</Badge>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -278,6 +285,77 @@ function NewsletterSection() {
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Currency Preference ─────────────────────────────────────────────────────
|
||||
|
||||
function CurrencySection() {
|
||||
const { profile, getProfile } = useProfile();
|
||||
const { setCurrency } = useCurrencyPreference();
|
||||
const [currencies, setCurrencies] = useState([]);
|
||||
const [localValue, setLocalValue] = useState("USD");
|
||||
const [saving, setSaving] = 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 savedValue = profile?.preferred_currency ?? "USD";
|
||||
const isDirty = localValue !== savedValue;
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
{isDirty && (
|
||||
<Button size="sm" onClick={handleSave} disabled={saving}>
|
||||
{saving ? "Saving…" : "Save"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Delete Account ───────────────────────────────────────────────────────────
|
||||
|
||||
function DeleteAccountSection({ logout }) {
|
||||
@@ -368,6 +446,10 @@ 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>
|
||||
|
||||
<Section icon={Trash2} title="Danger Zone" description="Irreversible account actions.">
|
||||
<DeleteAccountSection logout={logout} />
|
||||
</Section>
|
||||
|
||||
Reference in New Issue
Block a user