"use client"; import { AlertTriangle, Building, ChevronDown, Database, Download, Eye, EyeOff, FileText, FileUp, Info, Key, Palette, Shield, Upload, User, Users, Link as LinkIcon, } from "lucide-react"; import { authClient } from "~/lib/auth-client"; import * as React from "react"; import { useState } from "react"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "~/components/ui/alert-dialog"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "~/components/ui/collapsible"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "~/components/ui/dialog"; import { Input } from "~/components/ui/input"; import { Label } from "~/components/ui/label"; import { Textarea } from "~/components/ui/textarea"; import { api } from "~/trpc/react"; import { Switch } from "~/components/ui/switch"; import { Slider } from "~/components/ui/slider"; import { useAnimationPreferences } from "~/components/providers/animation-preferences-provider"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; export function SettingsContent() { const { data: session } = authClient.useSession(); // const session = { user: null } as any; const [name, setName] = useState(""); const [deleteConfirmText, setDeleteConfirmText] = useState(""); const [importData, setImportData] = useState(""); const [isImportDialogOpen, setIsImportDialogOpen] = useState(false); const [importMethod, setImportMethod] = useState<"file" | "paste">("file"); // Password change state const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [showCurrentPassword, setShowCurrentPassword] = useState(false); const [showNewPassword, setShowNewPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [isLinking, setIsLinking] = useState(false); const handleLinkAuthentik = async () => { setIsLinking(true); try { await authClient.linkSocial({ provider: "authentik", callbackURL: "/dashboard/settings", }); } catch (error) { toast.error("Failed to link account"); setIsLinking(false); } }; // Animation preferences via provider (centralized) const { prefersReducedMotion, animationSpeedMultiplier, updatePreferences, isUpdating: animationPrefsUpdating, setPrefersReducedMotion, setAnimationSpeedMultiplier, } = useAnimationPreferences(); const handleSaveAnimationPreferences = (e: React.FormEvent) => { e.preventDefault(); updatePreferences({ prefersReducedMotion, animationSpeedMultiplier, }); toast.success("Animation preferences updated"); }; // Queries const { data: profile, refetch: refetchProfile } = api.settings.getProfile.useQuery(); const { data: dataStats } = api.settings.getDataStats.useQuery(); // Mutations const updateProfileMutation = api.settings.updateProfile.useMutation({ onSuccess: () => { toast.success("Profile updated successfully"); void refetchProfile(); }, onError: (error: { message: string }) => { toast.error(`Failed to update profile: ${error.message}`); }, }); const changePasswordMutation = api.settings.changePassword.useMutation({ onSuccess: () => { toast.success("Password changed successfully"); setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); }, onError: (error: { message: string }) => { toast.error(`Failed to change password: ${error.message}`); }, }); const exportDataQuery = api.settings.exportData.useQuery(undefined, { enabled: false, }); // Handle download logic const handleDownload = React.useCallback((data: unknown) => { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json", }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `beenvoice-backup-${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast.success("Data backup downloaded successfully"); }, []); const importDataMutation = api.settings.importData.useMutation({ onSuccess: (result) => { toast.success( `Data imported successfully! Added ${result.imported.clients} clients, ${result.imported.businesses} businesses, and ${result.imported.invoices} invoices.`, ); setImportData(""); setIsImportDialogOpen(false); void refetchProfile(); }, onError: (error: { message: string }) => { toast.error(`Import failed: ${error.message}`); }, }); const deleteDataMutation = api.settings.deleteAllData.useMutation({ onSuccess: () => { toast.success("All data has been permanently deleted"); setDeleteConfirmText(""); }, onError: (error: { message: string }) => { toast.error(`Delete failed: ${error.message}`); }, }); const handleUpdateProfile = (e: React.FormEvent) => { e.preventDefault(); if (!name.trim()) { toast.error("Please enter your name"); return; } updateProfileMutation.mutate({ name: name.trim() }); }; const handleChangePassword = (e: React.FormEvent) => { e.preventDefault(); if (!currentPassword || !newPassword || !confirmPassword) { toast.error("Please fill in all password fields"); return; } if (newPassword !== confirmPassword) { toast.error("New passwords don't match"); return; } if (newPassword.length < 8) { toast.error("New password must be at least 8 characters"); return; } changePasswordMutation.mutate({ currentPassword, newPassword, confirmPassword, }); }; const handleExportData = async () => { try { const result = await exportDataQuery.refetch(); if (result.data) { handleDownload(result.data); } } catch (error) { toast.error( `Export failed: ${error instanceof Error ? error.message : "Unknown error"}`, ); } }; // Type guard for backup data const isValidBackupData = (data: unknown): boolean => { if (typeof data !== "object" || data === null) return false; const obj = data as Record; return !!( obj.exportDate && obj.version && obj.user && obj.clients && obj.businesses && obj.invoices && Array.isArray(obj.clients) && Array.isArray(obj.businesses) && Array.isArray(obj.invoices) ); }; const handleFileUpload = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; if (!file.name.endsWith(".json")) { toast.error("Please select a JSON file"); return; } const reader = new FileReader(); reader.onload = (e) => { try { const content = e.target?.result as string; const parsedData: unknown = JSON.parse(content); if (isValidBackupData(parsedData)) { // @ts-expect-error Server handles validation of backup data format importDataMutation.mutate(parsedData); } else { toast.error("Invalid backup file format"); } } catch { toast.error("Invalid JSON format. Please check your backup file."); } }; reader.onerror = () => { toast.error("Failed to read file"); }; reader.readAsText(file); }; const handleImportData = () => { try { const parsedData: unknown = JSON.parse(importData); if (isValidBackupData(parsedData)) { // @ts-expect-error Server handles validation of backup data format importDataMutation.mutate(parsedData); } else { toast.error("Invalid backup file format"); } } catch { toast.error("Invalid JSON format. Please check your backup file."); } }; const handleDeleteAllData = () => { if (deleteConfirmText !== "delete all my data") { toast.error("Please type 'delete all my data' to confirm"); return; } deleteDataMutation.mutate({ confirmText: deleteConfirmText }); }; // Set initial name value when profile loads React.useEffect(() => { if (profile?.name && !name) { setName(profile.name); } if (session?.user) { setName(session.user.name ?? ""); } }, [session, profile?.name, name]); // (Removed direct DOM mutation; provider handles applying preferences globally) const dataStatItems = [ { label: "Clients", value: dataStats?.clients ?? 0, icon: Users, color: "text-primary", bgColor: "bg-primary/10", }, { label: "Businesses", value: dataStats?.businesses ?? 0, icon: Building, color: "text-muted-foreground", bgColor: "bg-muted", }, { label: "Invoices", value: dataStats?.invoices ?? 0, icon: FileText, color: "text-primary", bgColor: "bg-accent", }, ]; return ( General Preferences Data
{/* Profile Section */} Profile Information Update your personal account details
setName(e.target.value)} placeholder="Enter your full name" />

Email address cannot be changed

{/* Security Settings */} Security Settings Change your password and manage account security
setCurrentPassword(e.target.value)} placeholder="Enter your current password" />
setNewPassword(e.target.value)} placeholder="Enter your new password" />

Password must be at least 8 characters long

setConfirmPassword(e.target.value)} placeholder="Confirm your new password" />
{/* Connected Accounts */} Connected Accounts Manage your linked social accounts and SSO providers

Authentik SSO

Connect your corporate account

{/* Theme follows system preferences automatically via CSS media queries */} {/* Accessibility & Animation */} Accessibility & Animation

Turn this on to reduce or remove non-essential animations and transitions.

setPrefersReducedMotion(Boolean(checked)) } aria-label="Toggle reduced motion" />
{prefersReducedMotion ? "1.00x (locked)" : `${animationSpeedMultiplier.toFixed(2)}x`}

Adjust global animation duration scaling. Lower values (0.25×, 0.5×, 0.75×) slow animations; higher values (2×, 3×, 4×) speed them up.

{/* Slider (desktop / larger screens) */}
(t === 1 ? "1x" : `${t}x`)} onValueChange={(v: number[]) => setAnimationSpeedMultiplier(v[0] ?? 1) } aria-label="Animation speed multiplier" className="mt-1" disabled={prefersReducedMotion} />
{/* Dropdown fallback (small screens) */}
{/* Data Overview */} Account Data Overview of your stored information
{dataStatItems.map((item, index) => { const Icon = item.icon; return (
{item.label}
{item.value}
); })}
{/* Data Management */} Data Management Backup, restore, or manage your account data
Import Backup Data Upload your backup JSON file or paste the contents below. This will add the data to your existing account.
{/* Import Method Selector */}
{/* File Upload Method */} {importMethod === "file" && (

Select the JSON backup file you previously exported.

)} {/* Manual Paste Method */} {importMethod === "paste" && (