spb/note-spboucher-ai
Public
1"use client";2import dynamic from "next/dynamic";3import { useEffect, useState } from "react";4import { useQuery } from "@tanstack/react-query";5import { User, ShieldCheck, Palette, Sparkles, Database, Info } from "lucide-react";6import { api } from "@/lib/client/api";7import { Tabs, TabsList, TabsTrigger, TabsContent, Spinner } from "@/components/ui/primitives";8import { ProfileSection } from "./ProfileSection";9import { SecuritySection } from "./SecuritySection";10import { AppearanceSection } from "./AppearanceSection";11import { AISection } from "./AISection";12import { AboutSection } from "./AboutSection";13import { applyPrefs } from "./prefs";1415export interface SettingsProfile { id: string; username: string; displayName: string; email: string | null; totpEnabled: boolean; preferences: Record<string, unknown>; createdAt: string; workspace: { id: string; name: string }; aiBudgetUsd: number; sessionId: string }16// @ts-ignore — DataSection is written by another engineer; a missing export falls back to a placeholder.17const DataSection = dynamic(() => import("@/components/settings/DataSection").then((m: any) => (m.DataSection ?? m.default ?? (() => <p className="text-[13px] text-fg-muted">Data tools are not available yet.</p>)) as React.ComponentType<any>).catch(() => (() => <p className="text-[13px] text-fg-muted">Data tools are not available yet.</p>) as React.ComponentType<any>), { ssr: false, loading: () => <div className="flex justify-center py-8"><Spinner /></div> });18const TABS = [{ id: "profile", label: "Profile", icon: <User /> }, { id: "security", label: "Security", icon: <ShieldCheck /> }, { id: "appearance", label: "Appearance", icon: <Palette /> }, { id: "ai", label: "AI", icon: <Sparkles /> }, { id: "data", label: "Data", icon: <Database /> }, { id: "about", label: "About", icon: <Info /> }];1920export function SettingsView({ version }: { version: string }) {21 const [tab, setTab] = useState("profile");22 useEffect(() => { const h = window.location.hash.slice(1); if (TABS.some((t) => t.id === h)) setTab(h); }, []);23 const { data: profile, refetch, isLoading } = useQuery({ queryKey: ["settings"], queryFn: () => api<SettingsProfile>("/api/settings") });24 useEffect(() => { if (profile) applyPrefs(profile.preferences as any); }, [profile]);25 const refresh = () => { refetch(); };26 return (27 <div className="mx-auto w-full max-w-4xl px-4 py-6 sm:px-6 sm:py-8">28 <h1 className="mb-4 text-2xl font-semibold tracking-tight">Settings</h1>29 <Tabs value={tab} onValueChange={(v) => { setTab(v); history.replaceState(null, "", `#${v}`); }}>30 <div className="-mx-4 mb-6 overflow-x-auto px-4 sm:mx-0 sm:px-0"><TabsList className="h-9">{TABS.map((t) => <TabsTrigger key={t.id} value={t.id} className="h-8 gap-1.5 [&_svg]:size-4">{t.icon}<span className="hidden sm:inline">{t.label}</span></TabsTrigger>)}</TabsList></div>31 {isLoading || !profile ? <div className="flex justify-center py-16"><Spinner /></div> : <>32 <TabsContent value="profile"><ProfileSection profile={profile} refresh={refresh} /></TabsContent>33 <TabsContent value="security"><SecuritySection profile={profile} refresh={refresh} /></TabsContent>34 <TabsContent value="appearance"><AppearanceSection profile={profile} refresh={refresh} /></TabsContent>35 <TabsContent value="ai"><AISection profile={profile} refresh={refresh} /></TabsContent>36 <TabsContent value="data"><DataSection /></TabsContent>37 <TabsContent value="about"><AboutSection version={version} /></TabsContent>38 </>}39 </Tabs>40 </div>41 );42}43