'use client'; import { useState } from 'react'; import { getSettings, saveSettings, resetSettings, type PortalSettings } from '@/lib/storage'; import PortalLayout from '@/app/layout-portal'; import { Settings, Globe, Server, HardDrive, RefreshCw, Sun, Moon, Save, RotateCcw, Check, AlertTriangle, } from 'lucide-react'; /* ── Helpers ── */ function formatStorageMB(mb: number): string { if (mb >= 1024) return (mb / 1024).toFixed(0) + ' GB'; return mb + ' MB'; } /* ════════════════════════════ Page ════════════════════════════ */ export default function SettingsPage() { const [form, setForm] = useState(() => getSettings()); const [original, setOriginal] = useState(() => getSettings()); const [saved, setSaved] = useState(false); const [error, setError] = useState(null); const [validation, setValidation] = useState>({}); const dirty = JSON.stringify(form) !== JSON.stringify(original); /* ── Field update ── */ function update(key: K, value: PortalSettings[K]) { setForm(prev => ({ ...prev, [key]: value })); setSaved(false); setError(null); // Clear validation error on change if (validation[key]) { setValidation(prev => ({ ...prev, [key]: null })); } } /* ── Validate ── */ function validate(): boolean { const errs: Record = {}; let valid = true; if (!form.gatewayUrl.startsWith('http://') && !form.gatewayUrl.startsWith('https://')) { errs.gatewayUrl = 'Must start with http:// or https://'; valid = false; } if (form.gatewayUrl.length > 500) { errs.gatewayUrl = 'Too long (max 500 chars)'; valid = false; } if (form.apiEndpoint && !form.apiEndpoint.startsWith('/') && !form.apiEndpoint.startsWith('http')) { errs.apiEndpoint = 'Must be empty, a path (/…), or a URL'; valid = false; } if (form.storageMax < 1 || form.storageMax > 102400) { errs.storageMax = 'Must be 1–102400 MB'; valid = false; } if (form.refreshInterval < 5 || form.refreshInterval > 300) { errs.refreshInterval = 'Must be 5–300 seconds'; valid = false; } setValidation(errs); if (!valid) setError('Fix validation errors before saving'); return valid; } /* ── Save ── */ function handleSave() { if (!validate()) return; const merged = saveSettings(form); setForm(merged); setOriginal({ ...merged }); setSaved(true); setError(null); setTimeout(() => setSaved(false), 2500); } /* ── Reset ── */ function handleReset() { if (!confirm('Reset all settings to defaults?')) return; const defaults = resetSettings(); setForm(defaults); setOriginal({ ...defaults }); setSaved(false); setError(null); setValidation({}); } /* ── Render helpers ── */ function inputCls(field: string): string { return `w-full rounded-lg bg-surface-900 border ${validation[field] ? 'border-accent-rose/50' : 'border-surface-700'} px-3 py-2 text-sm text-surface-200 placeholder:text-surface-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-colors`; } function errorMsg(field: string) { return validation[field] ? (

{validation[field]}

) : null; } /* ════════════ Sections ════════════ */ const sections = [ { title: 'IPFS Gateway', icon: Globe, iconColor: 'text-accent-cyan', fields: (
update('gatewayUrl', e.target.value)} placeholder="https://ipfs.io/ipfs" className={inputCls('gatewayUrl')} /> {errorMsg('gatewayUrl')}

Base URL for IPFS gateway links. Used in dashboard, history, and share links.

), }, { title: 'API Endpoint', icon: Server, iconColor: 'text-accent-purple', fields: (
update('apiEndpoint', e.target.value)} placeholder="(same-origin)" className={inputCls('apiEndpoint')} /> {errorMsg('apiEndpoint')}

Leave empty to use the same origin. Set to a custom URL to proxy through another server.

), }, { title: 'Storage', icon: HardDrive, iconColor: 'text-accent-amber', fields: (
update('storageMax', Number(e.target.value))} className="flex-1 accent-brand-500 h-1.5 rounded-full appearance-none bg-surface-700 cursor-pointer" style={{ accentColor: '#14b8a6' }} /> update('storageMax', Math.max(1, Math.min(102400, Number(e.target.value) || 0)))} className="w-20 rounded-lg bg-surface-900 border border-surface-700 px-2 py-1.5 text-xs text-surface-200 text-center focus:outline-none focus:border-brand-500" /> MB
{errorMsg('storageMax')}

Maximum storage the IPFS node should use. Applied server-side.

), }, { title: 'Auto-Refresh', icon: RefreshCw, iconColor: 'text-accent-green', fields: (
update('refreshInterval', Number(e.target.value))} className="flex-1 accent-brand-500 h-1.5 rounded-full appearance-none bg-surface-700 cursor-pointer" style={{ accentColor: '#14b8a6' }} /> update('refreshInterval', Math.max(5, Math.min(120, Number(e.target.value) || 0)))} className="w-16 rounded-lg bg-surface-900 border border-surface-700 px-2 py-1.5 text-xs text-surface-200 text-center focus:outline-none focus:border-brand-500" /> sec
{errorMsg('refreshInterval')}

How often the dashboard auto-refreshes peer/bandwidth data.

), }, { title: 'Theme', icon: form.theme === 'dark' ? Moon : Sun, iconColor: 'text-accent-amber', fields: (
{(['dark', 'light'] as const).map(theme => ( ))}

Theme preference is saved but only affects new sessions. Full theme switching coming soon.

), }, ]; return ( {/* ── Header ── */}

Settings

IPFS Portal configuration

{/* ── Settings grid ── */}
{sections.map((s) => (

{s.title}

{s.fields}
))}
{/* ── Actions + status ── */}
{/* Save */} {/* Reset */} {/* Dirty indicator */} {dirty && !saved && ( Unsaved changes )} {/* Error */} {error && ( {error} )}
); }