From 3cb5ab7d4e006eb751028093c6f71d0ad92e3838 Mon Sep 17 00:00:00 2001 From: hex-bot Date: Wed, 13 May 2026 14:16:20 -0400 Subject: [PATCH] CUB-128: Build Settings page --- frontend/src/App.tsx | 40 +++- frontend/src/pages/SettingsPage.tsx | 290 ++++++++++++++++++++++++++++ 2 files changed, 326 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/SettingsPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a5a8c3c..3c0dce9 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { BrowserRouter, Routes, Route } from 'react-router-dom' +import { BrowserRouter, Routes, Route, NavLink } from 'react-router-dom' +import { Package, Settings } from 'lucide-react' import InventoryPage from './pages/InventoryPage' +import SettingsPage from './pages/SettingsPage' const queryClient = new QueryClient() @@ -9,13 +11,43 @@ export default function App() {
-
-
E
-

Extrudex

+
+
+
E
+

Extrudex

+
+
} /> + } />
diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx new file mode 100644 index 0000000..79b7278 --- /dev/null +++ b/frontend/src/pages/SettingsPage.tsx @@ -0,0 +1,290 @@ +import { useState } from 'react' +import { Save, Wifi, WifiOff, Sun, Moon, AlertCircle, CheckCircle2 } from 'lucide-react' + +// TODO: Replace with API calls to GET/PUT /api/settings once Dex implements the endpoint +const DEFAULT_SETTINGS = { + lowStockThresholdGrams: 100, + filamentCrossSectionMm2: 2.405, // π × (0.875mm)² — standard 1.75mm filament + currency: 'USD', + theme: 'dark' as 'dark' | 'light' | 'system', +} + +const CURRENCIES = ['USD', 'EUR', 'GBP', 'CAD', 'AUD', 'JPY'] + +type ConnectionStatus = 'idle' | 'testing' | 'ok' | 'error' + +interface PrinterConfig { + name: string + type: 'bambu' | 'moonraker' + host: string +} + +// TODO: Load from GET /api/printers once Dex implements the endpoint +const MOCK_PRINTERS: PrinterConfig[] = [ + { name: 'Bambu X1C #1', type: 'bambu', host: '192.168.1.100' }, + { name: 'Bambu X1C #2', type: 'bambu', host: '192.168.1.101' }, + { name: 'Voron 2.4 (Klipper)', type: 'moonraker', host: '192.168.1.200' }, +] + +function SettingRow({ label, hint, children }: { label: string; hint?: string; children: React.ReactNode }) { + return ( +
+
+
{label}
+ {hint &&
{hint}
} +
+
{children}
+
+ ) +} + +function SectionHeader({ title, description }: { title: string; description?: string }) { + return ( +
+

{title}

+ {description &&

{description}

} +
+ ) +} + +export default function SettingsPage() { + const [lowStockThreshold, setLowStockThreshold] = useState(DEFAULT_SETTINGS.lowStockThresholdGrams) + const [crossSection, setCrossSection] = useState(DEFAULT_SETTINGS.filamentCrossSectionMm2) + const [currency, setCurrency] = useState(DEFAULT_SETTINGS.currency) + const [theme, setTheme] = useState(DEFAULT_SETTINGS.theme) + const [saved, setSaved] = useState(false) + const [saveError, setSaveError] = useState(false) + const [printerStatus, setPrinterStatus] = useState>({}) + + const [thresholdError, setThresholdError] = useState('') + const [crossSectionError, setCrossSectionError] = useState('') + + const validate = () => { + let valid = true + if (!lowStockThreshold || lowStockThreshold < 0 || lowStockThreshold > 10000) { + setThresholdError('Must be between 0 and 10,000 g') + valid = false + } else { + setThresholdError('') + } + if (!crossSection || crossSection <= 0 || crossSection > 100) { + setCrossSectionError('Must be a positive number (typical: 2.405 for 1.75mm)') + valid = false + } else { + setCrossSectionError('') + } + return valid + } + + const handleSave = async () => { + if (!validate()) return + try { + // TODO: PUT /api/settings with { low_stock_threshold_grams, filament_cross_section_mm2, currency, theme } + setSaved(true) + setSaveError(false) + setTimeout(() => setSaved(false), 3000) + } catch { + setSaveError(true) + setSaved(false) + } + } + + const testPrinterConnection = async (printer: PrinterConfig) => { + setPrinterStatus(s => ({ ...s, [printer.host]: 'testing' })) + // TODO: POST /api/printers/test with { host, type } — Dex to implement + await new Promise(r => setTimeout(r, 1500)) + // Mock: hosts ending in .100 or .200 pass, others fail + const ok = printer.host.endsWith('.100') || printer.host.endsWith('.200') + setPrinterStatus(s => ({ ...s, [printer.host]: ok ? 'ok' : 'error' })) + } + + return ( +
+ {/* Header */} +
+
+

Settings

+

App configuration for Extrudex

+
+
+ {saved && ( + + Saved + + )} + {saveError && ( + + Save failed + + )} + +
+
+ + {/* Inventory Settings */} +
+ + +
+
+ setLowStockThreshold(Number(e.target.value))} + className={`w-32 rounded-lg bg-slate-800 border ${thresholdError ? 'border-red-500' : 'border-slate-700'} px-3 py-2 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-emerald-500`} + /> + grams +
+ {thresholdError &&

{thresholdError}

} +
+
+ + +
+
+ setCrossSection(Number(e.target.value))} + className={`w-36 rounded-lg bg-slate-800 border ${crossSectionError ? 'border-red-500' : 'border-slate-700'} px-3 py-2 text-sm text-slate-100 focus:outline-none focus:ring-2 focus:ring-emerald-500`} + /> + mm² +
+ {crossSectionError &&

{crossSectionError}

} +

1.75mm filament = 2.405 mm² · 2.85mm filament = 6.379 mm²

+
+
+ + + + +
+ + {/* Appearance */} +
+ + +
+ {(['system', 'dark', 'light'] as const).map(t => ( + + ))} +
+
+
+ + {/* Printer Connections */} +
+ + {/* TODO: Load printer list from GET /api/printers */} + {MOCK_PRINTERS.map(printer => { + const status = printerStatus[printer.host] ?? 'idle' + return ( + +
+ + +
+
+ ) + })} +
+ + {/* Save footer (mobile convenience) */} +
+ +
+
+ ) +} + +function StatusBadge({ status }: { status: ConnectionStatus }) { + if (status === 'idle') return null + if (status === 'testing') { + return ( + + Testing… + + ) + } + if (status === 'ok') { + return ( + + Connected + + ) + } + return ( + + Unreachable + + ) +}