Spaces:
Running
Running
| "use client"; | |
| import { useEffect, useState } from "react"; | |
| import { motion } from "framer-motion"; | |
| import { User, Bell, Sparkles, Shield, Save, Loader2, CheckCircle2 } from "lucide-react"; | |
| import { authApi } from "@/lib/api"; | |
| import { useAuthStore } from "@/lib/stores/authStore"; | |
| const PERSONALITIES = [ | |
| "Balanced Investor", | |
| "Conservative Saver", | |
| "Growth Focused", | |
| "Aggressive Trader", | |
| "Debt Reducer", | |
| ]; | |
| export default function SettingsPage() { | |
| const { user, restoreSession } = useAuthStore(); | |
| const [loading, setLoading] = useState(true); | |
| const [saving, setSaving] = useState(false); | |
| const [saved, setSaved] = useState(false); | |
| const [form, setForm] = useState({ | |
| name: "", | |
| email: "", | |
| financial_personality: "Balanced Investor", | |
| notifications_enabled: true, | |
| ai_coaching_enabled: true, | |
| fraud_alerts_enabled: true, | |
| }); | |
| useEffect(() => { | |
| (async () => { | |
| try { | |
| const me = await authApi.me(); | |
| setForm({ | |
| name: me.name, | |
| email: me.email, | |
| financial_personality: me.financial_personality || "Balanced Investor", | |
| notifications_enabled: me.notifications_enabled ?? true, | |
| ai_coaching_enabled: me.ai_coaching_enabled ?? true, | |
| fraud_alerts_enabled: me.fraud_alerts_enabled ?? true, | |
| }); | |
| } catch { | |
| if (user) { | |
| setForm((f) => ({ | |
| ...f, | |
| name: user.name, | |
| email: user.email, | |
| financial_personality: user.financial_personality || "Balanced Investor", | |
| })); | |
| } | |
| } finally { | |
| setLoading(false); | |
| } | |
| })(); | |
| }, [user]); | |
| const handleSave = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| setSaving(true); | |
| setSaved(false); | |
| try { | |
| await authApi.updateSettings({ | |
| name: form.name, | |
| financial_personality: form.financial_personality, | |
| notifications_enabled: form.notifications_enabled, | |
| ai_coaching_enabled: form.ai_coaching_enabled, | |
| fraud_alerts_enabled: form.fraud_alerts_enabled, | |
| }); | |
| await restoreSession(); | |
| setSaved(true); | |
| setTimeout(() => setSaved(false), 3000); | |
| } catch { | |
| /* ignore */ | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| const Toggle = ({ | |
| label, | |
| desc, | |
| checked, | |
| onChange, | |
| }: { | |
| label: string; | |
| desc: string; | |
| checked: boolean; | |
| onChange: (v: boolean) => void; | |
| }) => ( | |
| <div className="flex items-center justify-between gap-4 py-3 border-b border-white/5 last:border-0"> | |
| <div> | |
| <p className="text-sm font-medium text-white">{label}</p> | |
| <p className="text-xs text-zinc-500 mt-0.5">{desc}</p> | |
| </div> | |
| <button | |
| type="button" | |
| role="switch" | |
| aria-checked={checked} | |
| onClick={() => onChange(!checked)} | |
| className={`relative h-6 w-11 rounded-full transition-colors ${checked ? "bg-emerald-500" : "bg-zinc-700"}`} | |
| > | |
| <span | |
| className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${checked ? "left-5" : "left-0.5"}`} | |
| /> | |
| </button> | |
| </div> | |
| ); | |
| if (loading) { | |
| return ( | |
| <div className="flex justify-center py-20 text-zinc-500 text-sm">Loading settings...</div> | |
| ); | |
| } | |
| return ( | |
| <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex flex-col gap-6 max-w-2xl"> | |
| <div> | |
| <h1 className="text-3xl font-bold tracking-tight text-white">Settings</h1> | |
| <p className="text-zinc-400 mt-1 text-sm">Account preferences and AI personalization</p> | |
| </div> | |
| <form onSubmit={handleSave} className="space-y-6"> | |
| <section className="glass-card p-6"> | |
| <h2 className="text-base font-semibold text-white flex items-center gap-2 mb-4"> | |
| <User className="h-4 w-4 text-blue-400" /> | |
| Profile | |
| </h2> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="text-xs text-zinc-400">Display name</label> | |
| <input | |
| value={form.name} | |
| onChange={(e) => setForm({ ...form, name: e.target.value })} | |
| className="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white focus:border-emerald-500/40 focus:outline-none" | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-xs text-zinc-400">Email</label> | |
| <input | |
| value={form.email} | |
| disabled | |
| className="mt-1 w-full rounded-xl border border-white/10 bg-white/[0.02] px-3 py-2 text-sm text-zinc-500 cursor-not-allowed" | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-xs text-zinc-400">Financial personality</label> | |
| <select | |
| value={form.financial_personality} | |
| onChange={(e) => setForm({ ...form, financial_personality: e.target.value })} | |
| className="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-3 py-2 text-sm text-white focus:border-emerald-500/40 focus:outline-none" | |
| > | |
| {PERSONALITIES.map((p) => ( | |
| <option key={p} value={p} className="bg-zinc-900"> | |
| {p} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| </div> | |
| </section> | |
| <section className="glass-card p-6"> | |
| <h2 className="text-base font-semibold text-white flex items-center gap-2 mb-2"> | |
| <Bell className="h-4 w-4 text-amber-400" /> | |
| Notifications & AI | |
| </h2> | |
| <Toggle | |
| label="Push notifications" | |
| desc="Fraud alerts, insights, and budget warnings" | |
| checked={form.notifications_enabled} | |
| onChange={(v) => setForm({ ...form, notifications_enabled: v })} | |
| /> | |
| <Toggle | |
| label="AI coaching" | |
| desc="Weekly coach reports and dashboard briefings" | |
| checked={form.ai_coaching_enabled} | |
| onChange={(v) => setForm({ ...form, ai_coaching_enabled: v })} | |
| /> | |
| <Toggle | |
| label="Fraud alerts" | |
| desc="Real-time suspicious activity notifications" | |
| checked={form.fraud_alerts_enabled} | |
| onChange={(v) => setForm({ ...form, fraud_alerts_enabled: v })} | |
| /> | |
| </section> | |
| <section className="glass-card p-6 flex items-start gap-3"> | |
| <Shield className="h-5 w-5 text-emerald-400 flex-shrink-0 mt-0.5" /> | |
| <div className="text-sm text-zinc-400"> | |
| <p className="text-white font-medium">Security</p> | |
| <p className="mt-1">Sessions use JWT encryption. Password changes require backend support in this demo.</p> | |
| </div> | |
| </section> | |
| <button | |
| type="submit" | |
| disabled={saving} | |
| className="flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-emerald-500 to-cyan-500 px-6 py-3 text-sm font-semibold text-white disabled:opacity-50 w-full sm:w-auto" | |
| > | |
| {saving ? ( | |
| <Loader2 className="h-4 w-4 animate-spin" /> | |
| ) : saved ? ( | |
| <CheckCircle2 className="h-4 w-4" /> | |
| ) : ( | |
| <Save className="h-4 w-4" /> | |
| )} | |
| {saved ? "Saved" : "Save changes"} | |
| </button> | |
| </form> | |
| <div className="glass-card p-4 flex items-center gap-2 text-xs text-zinc-500"> | |
| <Sparkles className="h-4 w-4 text-cyan-400" /> | |
| User ID: {user?.user_id?.slice(0, 8) ?? "—"}… | |
| </div> | |
| </motion.div> | |
| ); | |
| } | |