Add Settings page with Profile, Appearance, Integrations, Notifications, Data & Backup, About sections

This commit is contained in:
Claude 2026-06-18 18:44:26 +00:00
parent f87e88420c
commit e386e327b4
No known key found for this signature in database
2 changed files with 511 additions and 0 deletions

View file

@ -5,6 +5,7 @@ import TopBar from './components/TopBar'
import Glance from './pages/Glance'
import Infrastructure from './pages/Infrastructure'
import BookNest from './pages/BookNest'
import Settings from './pages/Settings'
function App() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
@ -60,6 +61,7 @@ function App() {
<Route path="/" element={<Glance />} />
<Route path="/infrastructure" element={<Infrastructure />} />
<Route path="/booknest" element={<BookNest />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</section>
</main>

509
src/pages/Settings.tsx Normal file
View file

@ -0,0 +1,509 @@
import { useState } from 'react'
import {
User,
Palette,
Plug,
Bell,
Database,
Info,
Eye,
EyeOff,
Check,
Download,
Upload,
Trash2,
RotateCcw,
} 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 IntegrationField = { label: string; value: string; secret?: boolean }
const integrations: { name: string; online: boolean; fields: IntegrationField[] }[] = [
{ name: 'Proxmox', online: true, fields: [{ label: 'Host URL', value: 'https://pve1.local:8006' }, { label: 'API Token', value: '••••••••••••', secret: true }] },
{ name: 'Docker', online: true, fields: [{ label: 'Socket / Remote URL', value: 'unix:///var/run/docker.sock' }] },
{ name: 'NetBird', online: true, fields: [{ label: 'API Key', value: '••••••••••••', secret: true }] },
{ name: 'Cloudflare', online: true, fields: [{ label: 'API Token', value: '••••••••••••', secret: true }, { label: 'Zone ID', value: 'a1b2c3d4e5f6' }] },
{ name: 'AWS', online: true, fields: [{ label: 'Access Key', value: 'AKIA••••••••' }, { label: 'Secret', value: '••••••••••••', secret: true }, { label: 'Region', value: 'us-east-1' }] },
{ name: 'Uptime Kuma', online: false, fields: [{ label: 'URL', value: '' }, { label: 'API Key', value: '', secret: true }] },
{ name: 'Weather API', online: true, fields: [{ label: 'Location', value: 'Charlotte, NC' }, { label: 'Units', value: 'Imperial' }] },
]
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 (
<button
onClick={onClick}
className="cursor-pointer border-none"
style={{
width: '38px',
height: '20px',
borderRadius: '10px',
backgroundColor: on ? '#C8A434' : 'rgba(255,255,255,0.08)',
position: 'relative',
transition: 'background-color 0.2s ease',
flexShrink: 0,
}}
>
<span
style={{
position: 'absolute',
top: '2px',
left: on ? '20px' : '2px',
width: '16px',
height: '16px',
borderRadius: '50%',
backgroundColor: '#0A0B0D',
transition: 'left 0.2s ease',
}}
/>
</button>
)
}
function GoldButton({ children, danger }: { children: React.ReactNode; danger?: boolean }) {
return (
<button
className="flex items-center gap-2 cursor-pointer transition-colors whitespace-nowrap"
style={{
fontSize: '12px',
fontWeight: 600,
color: danger ? '#E74C3C' : '#0A0B0D',
backgroundColor: danger ? 'transparent' : '#C8A434',
border: danger ? '1px solid rgba(231,76,60,0.4)' : 'none',
borderRadius: '8px',
padding: '9px 16px',
boxShadow: danger ? 'none' : '0 0 14px rgba(200,164,52,0.2)',
}}
>
{children}
</button>
)
}
function ProfileSection() {
return (
<div style={cardBase}>
<h3 style={sectionTitle}>Profile</h3>
<div className="flex items-center gap-4" style={{ marginBottom: '24px' }}>
<div
className="rounded-full border-2 flex items-center justify-center font-bold"
style={{ width: '64px', height: '64px', borderColor: '#C8A434', color: '#C8A434', fontSize: '20px', backgroundColor: 'rgba(200,164,52,0.08)' }}
>
AO
</div>
<div>
<div className="flex items-center gap-2">
<span style={{ fontSize: '15px', color: '#E8E6E0', fontWeight: 600 }}>ArchNest Ops</span>
<span style={{ fontSize: '10px', color: '#C8A434', border: '1px solid rgba(200,164,52,0.3)', borderRadius: '6px', padding: '2px 8px' }}>
Administrator
</span>
</div>
<span style={{ fontSize: '12px', color: '#7A7D85' }}>admin@archnest.io</span>
</div>
</div>
<div className="grid grid-cols-2 gap-4" style={{ marginBottom: '20px' }}>
<div>
<label style={labelStyle}>Display Name</label>
<input style={inputStyle} defaultValue="ArchNest Ops" />
</div>
<div>
<label style={labelStyle}>Email</label>
<input style={inputStyle} defaultValue="admin@archnest.io" />
</div>
<div>
<label style={labelStyle}>Role</label>
<input style={inputStyle} defaultValue="Administrator" disabled />
</div>
<div>
<label style={labelStyle}>Timezone</label>
<input style={inputStyle} defaultValue="America/New_York (EST)" />
</div>
</div>
<GoldButton>
<Check size={14} />
Save Changes
</GoldButton>
</div>
)
}
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 (
<div style={cardBase}>
<h3 style={sectionTitle}>Appearance</h3>
<div className="flex items-center justify-between" style={{ marginBottom: '20px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Theme</span>
<div className="flex items-center gap-1" style={{ backgroundColor: 'rgba(255,255,255,0.03)', borderRadius: '8px', padding: '3px' }}>
{(['dark', 'light'] as const).map((t) => (
<button
key={t}
onClick={() => 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}
</button>
))}
</div>
</div>
<div style={{ marginBottom: '20px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0', marginBottom: '10px', display: 'block' }}>Accent Color</span>
<div className="flex items-center gap-3">
{accentColors.map((a) => (
<button
key={a.name}
onClick={() => 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 && <Check size={14} color="#0A0B0D" />}
</button>
))}
</div>
</div>
<div style={{ marginBottom: '20px' }}>
<div className="flex items-center justify-between" style={{ marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Font Size</span>
<span style={{ fontSize: '11px', color: '#C8A434' }}>{fontSize}px</span>
</div>
<input
type="range"
min={12}
max={16}
value={fontSize}
onChange={(e) => setFontSize(Number(e.target.value))}
className="w-full"
style={{ accentColor: '#C8A434' }}
/>
</div>
<div style={{ marginBottom: '20px' }}>
<div className="flex items-center justify-between" style={{ marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Card Border Radius</span>
<span style={{ fontSize: '11px', color: '#C8A434' }}>{radius}px</span>
</div>
<input
type="range"
min={4}
max={16}
value={radius}
onChange={(e) => setRadius(Number(e.target.value))}
className="w-full"
style={{ accentColor: '#C8A434' }}
/>
</div>
<div className="flex items-center justify-between" style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Sidebar Expanded by Default</span>
<Toggle on={sidebarExpanded} onClick={() => setSidebarExpanded((v) => !v)} />
</div>
<div className="flex items-center justify-between">
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Animations</span>
<Toggle on={animations} onClick={() => setAnimations((v) => !v)} />
</div>
</div>
)
}
function IntegrationsSection() {
const [revealed, setRevealed] = useState<Set<string>>(new Set())
function toggleReveal(key: string) {
setRevealed((prev) => {
const next = new Set(prev)
if (next.has(key)) next.delete(key)
else next.add(key)
return next
})
}
return (
<div className="flex flex-col gap-4">
{integrations.map((integ) => (
<div key={integ.name} style={cardBase}>
<div className="flex items-center justify-between" style={{ marginBottom: '16px' }}>
<div className="flex items-center gap-2.5">
<span
style={{
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: integ.online ? '#2ECC71' : '#4A4D55',
boxShadow: integ.online ? '0 0 6px rgba(46,204,113,0.6)' : 'none',
}}
/>
<span style={{ fontSize: '13px', color: '#E8E6E0', fontWeight: 600 }}>{integ.name}</span>
</div>
<button
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',
}}
>
Test Connection
</button>
</div>
<div className="grid grid-cols-3 gap-4">
{integ.fields.map((f) => {
const key = `${integ.name}-${f.label}`
const isRevealed = revealed.has(key)
return (
<div key={key}>
<label style={labelStyle}>{f.label}</label>
<div className="relative">
<input
style={inputStyle}
type={f.secret && !isRevealed ? 'password' : 'text'}
defaultValue={f.value}
placeholder={f.value ? undefined : 'Not configured'}
/>
{f.secret && (
<button
onClick={() => toggleReveal(key)}
className="absolute cursor-pointer border-none bg-transparent"
style={{ right: '8px', top: '50%', transform: 'translateY(-50%)', color: '#7A7D85' }}
>
{isRevealed ? <EyeOff size={13} /> : <Eye size={13} />}
</button>
)}
</div>
</div>
)
})}
</div>
</div>
))}
</div>
)
}
function NotificationsSection() {
const [enabled, setEnabled] = useState(true)
const [email, setEmail] = useState(true)
const [push, setPush] = useState(false)
const [sound, setSound] = useState(true)
return (
<div style={cardBase}>
<h3 style={sectionTitle}>Notifications</h3>
<div className="flex items-center justify-between" style={{ marginBottom: '20px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Enable Notifications</span>
<Toggle on={enabled} onClick={() => setEnabled((v) => !v)} />
</div>
<div style={{ marginBottom: '20px' }}>
<label style={labelStyle}>Alert Threshold</label>
<select style={{ ...inputStyle, width: '220px' }} defaultValue="all">
<option value="all">All</option>
<option value="critical">Critical Only</option>
<option value="warning">Warning & Above</option>
</select>
</div>
<div className="flex items-center justify-between" style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Email Notifications</span>
<Toggle on={email} onClick={() => setEmail((v) => !v)} />
</div>
<div className="flex items-center justify-between" style={{ marginBottom: '16px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Browser Push</span>
<Toggle on={push} onClick={() => setPush((v) => !v)} />
</div>
<div className="flex items-center justify-between" style={{ marginBottom: '8px' }}>
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Sound</span>
<Toggle on={sound} onClick={() => setSound((v) => !v)} />
</div>
{sound && (
<input type="range" min={0} max={100} defaultValue={70} className="w-full" style={{ accentColor: '#C8A434' }} />
)}
</div>
)
}
function DataBackupSection() {
return (
<div style={cardBase}>
<h3 style={sectionTitle}>Data & Backup</h3>
<div className="flex flex-col gap-3" style={{ maxWidth: '320px' }}>
<div className="flex items-center justify-between">
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Export Bookmarks (JSON)</span>
<GoldButton><Download size={13} /> Export</GoldButton>
</div>
<div className="flex items-center justify-between">
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Import Bookmarks (JSON)</span>
<GoldButton><Upload size={13} /> Import</GoldButton>
</div>
<div className="flex items-center justify-between">
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Export Settings</span>
<GoldButton><Download size={13} /> Export</GoldButton>
</div>
<div className="border-t" style={{ borderColor: 'rgba(231,76,60,0.15)', margin: '8px 0' }} />
<div className="flex items-center justify-between">
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Clear Cache</span>
<GoldButton danger><Trash2 size={13} /> Clear</GoldButton>
</div>
<div className="flex items-center justify-between">
<span style={{ fontSize: '13px', color: '#E8E6E0' }}>Reset to Defaults</span>
<GoldButton danger><RotateCcw size={13} /> Reset</GoldButton>
</div>
</div>
</div>
)
}
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 (
<div style={cardBase}>
<h3 style={sectionTitle}>About</h3>
<div className="flex flex-col gap-3">
{rows.map(([label, value]) => (
<div key={label} className="flex items-center justify-between">
<span style={{ fontSize: '12px', color: '#7A7D85' }}>{label}</span>
<span style={{ fontSize: '12px', color: '#E8E6E0' }}>{value}</span>
</div>
))}
</div>
</div>
)
}
const sectionComponents: Record<string, () => 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 (
<div className="flex h-full w-full gap-5">
{/* Settings nav */}
<div className="flex flex-col gap-1 shrink-0" style={{ width: '200px' }}>
{navSections.map((s) => {
const Icon = s.icon
const isActive = active === s.id
return (
<button
key={s.id}
onClick={() => 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',
}}
>
<Icon size={15} />
{s.label}
</button>
)
})}
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto" style={{ scrollbarWidth: 'none' }}>
<ActiveSection />
</div>
</div>
)
}