import { useEffect, useRef, useState } from 'react' import { api, ApiError, type Integration } from '../lib/api' import { useAuth } from '../lib/AuthContext' import { User, Palette, Plug, Bell, Database, Info, Eye, EyeOff, Check, Download, Upload, Trash2, RotateCcw, Camera, } from 'lucide-react' const navSections = [ { id: 'profile', label: 'Profile', icon: User }, { id: 'appearance', label: 'Appearance', icon: Palette }, { id: 'integrations', label: 'Integrations', icon: Plug }, { id: 'notifications', label: 'Notifications', icon: Bell }, { id: 'data', label: 'Data & Backup', icon: Database }, { id: 'about', label: 'About', icon: Info }, ] const accentColors = [ { name: 'Gold', color: '#C8A434' }, { name: 'Teal', color: '#2DD4BF' }, { name: 'Purple', color: '#A855F7' }, { name: 'Blue', color: '#3B82F6' }, { name: 'Green', color: '#2ECC71' }, { name: 'Red', color: '#E74C3C' }, ] type FieldDef = { key: string; label: string; secret?: boolean } const integrationTypeDefs: { type: string; name: string; fields: FieldDef[] }[] = [ { type: 'proxmox', name: 'Proxmox', fields: [{ key: 'baseUrl', label: 'Host URL' }, { key: 'apiKey', label: 'API Token', secret: true }] }, { type: 'docker', name: 'Docker', fields: [{ key: 'baseUrl', label: 'Socket / Remote URL' }] }, { type: 'netbird', name: 'NetBird', fields: [{ key: 'apiKey', label: 'API Key', secret: true }] }, { type: 'cloudflare', name: 'Cloudflare', fields: [{ key: 'apiKey', label: 'API Token', secret: true }, { key: 'zoneId', label: 'Zone ID' }] }, { type: 'aws', name: 'AWS', fields: [{ key: 'accessKey', label: 'Access Key' }, { key: 'secretKey', label: 'Secret', secret: true }, { key: 'region', label: 'Region' }] }, { type: 'uptime_kuma', name: 'Uptime Kuma', fields: [{ key: 'baseUrl', label: 'URL' }, { key: 'apiKey', label: 'API Key', secret: true }] }, { type: 'weather', name: 'Weather API', fields: [{ key: 'location', label: 'Location' }, { key: 'units', label: 'Units' }] }, ] const cardBase: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.92)', border: '1px solid rgba(200, 164, 52, 0.08)', borderRadius: '12px', padding: '22px', position: 'relative', } const sectionTitle: React.CSSProperties = { fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1.5px', color: '#7A7D85', fontWeight: 500, marginBottom: '16px', } const labelStyle: React.CSSProperties = { fontSize: '11px', color: '#7A7D85', marginBottom: '6px', display: 'block', } const inputStyle: React.CSSProperties = { width: '100%', height: '34px', borderRadius: '8px', border: '1px solid rgba(200,164,52,0.12)', backgroundColor: 'rgba(255,255,255,0.03)', color: '#E8E6E0', fontSize: '12px', padding: '0 12px', outline: 'none', } function Toggle({ on, onClick }: { on: boolean; onClick: () => void }) { return ( ) } function GoldButton({ children, danger, onClick, disabled }: { children: React.ReactNode; danger?: boolean; onClick?: () => void; disabled?: boolean }) { return ( {children} ) } function ProfileSection() { const { user, setUser } = useAuth() const fileInputRef = useRef(null) const [displayName, setDisplayName] = useState(user?.display_name ?? '') const [email, setEmail] = useState(user?.email ?? '') const [avatar, setAvatar] = useState(user?.avatar_data_url ?? null) const [saving, setSaving] = useState(false) const [savedMsg, setSavedMsg] = useState('') useEffect(() => { setDisplayName(user?.display_name ?? '') setEmail(user?.email ?? '') setAvatar(user?.avatar_data_url ?? null) }, [user]) const initials = (displayName || user?.username || '?').slice(0, 2).toUpperCase() function handleAvatarChange(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return const reader = new FileReader() reader.onload = () => setAvatar(reader.result as string) reader.readAsDataURL(file) } async function handleSave() { setSaving(true) setSavedMsg('') try { const { user: updated } = await api.updateMe({ displayName, email, avatarDataUrl: avatar }) setUser(updated) setSavedMsg('Saved') } catch (err) { setSavedMsg(err instanceof ApiError ? err.message : 'Failed to save') } finally { setSaving(false) } } return ( Profile fileInputRef.current?.click()} className="relative rounded-full border-2 flex items-center justify-center font-bold cursor-pointer group" style={{ width: '64px', height: '64px', borderColor: '#C8A434', color: '#C8A434', fontSize: '20px', backgroundColor: 'rgba(200,164,52,0.08)', backgroundImage: avatar ? `url(${avatar})` : undefined, backgroundSize: 'cover', backgroundPosition: 'center', overflow: 'hidden', }} title="Upload photo" > {!avatar && initials} {displayName || user?.username} {email || 'No email set'} Display Name setDisplayName(e.target.value)} /> Email setEmail(e.target.value)} /> {saving ? 'Saving…' : 'Save Changes'} {savedMsg && {savedMsg}} ) } function AppearanceSection() { const [theme, setTheme] = useState<'dark' | 'light'>('dark') const [accent, setAccent] = useState('Gold') const [fontSize, setFontSize] = useState(13) const [radius, setRadius] = useState(12) const [sidebarExpanded, setSidebarExpanded] = useState(true) const [animations, setAnimations] = useState(true) return ( Appearance Theme {(['dark', 'light'] as const).map((t) => ( setTheme(t)} className="cursor-pointer border-none capitalize" style={{ fontSize: '11px', padding: '6px 14px', borderRadius: '6px', color: theme === t ? '#0A0B0D' : '#7A7D85', backgroundColor: theme === t ? '#C8A434' : 'transparent', fontWeight: 600, }} > {t} ))} Accent Color {accentColors.map((a) => ( setAccent(a.name)} title={a.name} className="cursor-pointer border-none rounded-full flex items-center justify-center" style={{ width: '28px', height: '28px', backgroundColor: a.color, outline: accent === a.name ? `2px solid ${a.color}` : 'none', outlineOffset: '3px', }} > {accent === a.name && } ))} Font Size {fontSize}px setFontSize(Number(e.target.value))} className="w-full" style={{ accentColor: '#C8A434' }} /> Card Border Radius {radius}px setRadius(Number(e.target.value))} className="w-full" style={{ accentColor: '#C8A434' }} /> Sidebar Expanded by Default setSidebarExpanded((v) => !v)} /> Animations setAnimations((v) => !v)} /> ) } function IntegrationsSection() { const [integrations, setIntegrations] = useState(null) const [revealed, setRevealed] = useState>(new Set()) const [drafts, setDrafts] = useState>>({}) const [statusMsg, setStatusMsg] = useState>({}) const [busy, setBusy] = useState>(new Set()) useEffect(() => { api.listIntegrations().then(({ integrations }) => setIntegrations(integrations)) }, []) function toggleReveal(key: string) { setRevealed((prev) => { const next = new Set(prev) if (next.has(key)) next.delete(key) else next.add(key) return next }) } function setBusyFlag(type: string, value: boolean) { setBusy((prev) => { const next = new Set(prev) if (value) next.add(type) else next.delete(type) return next }) } function setDraftField(type: string, fieldKey: string, value: string) { setDrafts((prev) => ({ ...prev, [type]: { ...prev[type], [fieldKey]: value } })) } async function handleSave(def: (typeof integrationTypeDefs)[number], existing: Integration | undefined) { setBusyFlag(def.type, true) setStatusMsg((prev) => ({ ...prev, [def.type]: '' })) try { const draft = drafts[def.type] ?? {} const config: Record = {} const secrets: Record = {} for (const f of def.fields) { const value = draft[f.key] if (value === undefined) continue if (f.secret) secrets[f.key] = value else config[f.key] = value } let integration: Integration if (existing) { ;({ integration } = await api.updateIntegration(existing.id, { config, secrets })) } else { ;({ integration } = await api.createIntegration({ type: def.type, name: def.name, config, secrets })) } setIntegrations((prev) => { const others = (prev ?? []).filter((i) => i.id !== integration.id) return [...others, integration] }) setStatusMsg((prev) => ({ ...prev, [def.type]: 'Saved' })) } catch (err) { setStatusMsg((prev) => ({ ...prev, [def.type]: err instanceof ApiError ? err.message : 'Save failed' })) } finally { setBusyFlag(def.type, false) } } async function handleTest(def: (typeof integrationTypeDefs)[number], existing: Integration | undefined) { if (!existing) { setStatusMsg((prev) => ({ ...prev, [def.type]: 'Save the integration before testing' })) return } setBusyFlag(def.type, true) try { const result = await api.testIntegration(existing.id) setStatusMsg((prev) => ({ ...prev, [def.type]: result.message })) const { integrations } = await api.listIntegrations() setIntegrations(integrations) } catch (err) { setStatusMsg((prev) => ({ ...prev, [def.type]: err instanceof ApiError ? err.message : 'Test failed' })) } finally { setBusyFlag(def.type, false) } } if (!integrations) { return ( Loading integrations… ) } return ( {integrationTypeDefs.map((def) => { const existing = integrations.find((i) => i.type === def.type) const online = existing?.status === 'connected' const draft = drafts[def.type] ?? {} return ( {def.name} {statusMsg[def.type] && ( {statusMsg[def.type]} )} handleSave(def, existing)} disabled={busy.has(def.type)} className="cursor-pointer border-none" style={{ fontSize: '11px', fontWeight: 600, color: '#0A0B0D', backgroundColor: '#C8A434', borderRadius: '6px', padding: '6px 12px', opacity: busy.has(def.type) ? 0.6 : 1, }} > Save handleTest(def, existing)} disabled={busy.has(def.type)} className="cursor-pointer border-none" style={{ fontSize: '11px', fontWeight: 600, color: '#C8A434', backgroundColor: 'rgba(200,164,52,0.08)', border: '1px solid rgba(200,164,52,0.2)', borderRadius: '6px', padding: '6px 12px', opacity: busy.has(def.type) ? 0.6 : 1, }} > Test Connection {def.fields.map((f) => { const key = `${def.type}-${f.key}` const isRevealed = revealed.has(key) const savedValue = f.secret ? '' : existing?.config[f.key] ?? '' const value = draft[f.key] ?? savedValue return ( {f.label} setDraftField(def.type, f.key, e.target.value)} placeholder={f.secret && existing ? '••••••••••••' : 'Not configured'} /> {f.secret && ( toggleReveal(key)} className="absolute cursor-pointer border-none bg-transparent" style={{ right: '8px', top: '50%', transform: 'translateY(-50%)', color: '#7A7D85' }} > {isRevealed ? : } )} ) })} ) })} ) } function NotificationsSection() { const [enabled, setEnabled] = useState(true) const [email, setEmail] = useState(true) const [push, setPush] = useState(false) const [sound, setSound] = useState(true) return ( Notifications Enable Notifications setEnabled((v) => !v)} /> Alert Threshold All Critical Only Warning & Above Email Notifications setEmail((v) => !v)} /> Browser Push setPush((v) => !v)} /> Sound setSound((v) => !v)} /> {sound && ( )} ) } function DataBackupSection() { return ( Data & Backup Export Bookmarks (JSON) Export Import Bookmarks (JSON) Import Export Settings Export Clear Cache Clear Reset to Defaults Reset ) } function AboutSection() { const rows: [string, string][] = [ ['App', 'ArchNest Dashboard v1.0.0'], ['Author', 'Samuel James'], ['Repo', 'github.com/SamuelSJames/archnest'], ['Stack', 'React 19, Vite, TypeScript'], ['License', 'MIT'], ] return ( About {rows.map(([label, value]) => ( {label} {value} ))} ) } const sectionComponents: Record JSX.Element> = { profile: ProfileSection, appearance: AppearanceSection, integrations: IntegrationsSection, notifications: NotificationsSection, data: DataBackupSection, about: AboutSection, } export default function Settings() { const [active, setActive] = useState('profile') const ActiveSection = sectionComponents[active] return ( {/* Settings nav */} {navSections.map((s) => { const Icon = s.icon const isActive = active === s.id return ( setActive(s.id)} className="flex items-center gap-2.5 cursor-pointer border-none bg-transparent transition-colors" style={{ fontSize: '13px', fontWeight: 500, padding: '10px 14px', borderRadius: '8px', color: isActive ? '#C8A434' : '#7A7D85', backgroundColor: isActive ? 'rgba(200,164,52,0.1)' : 'transparent', }} > {s.label} ) })} {/* Content */} ) }
Loading integrations…