import { useState, useEffect, useRef } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faUpload } from '@fortawesome/pro-duotone-svg-icons'; import { Input } from '@/components/base/input/input'; import { Select } from '@/components/base/select/select'; import { Checkbox } from '@/components/base/checkbox/checkbox'; import { Button } from '@/components/base/buttons/button'; import { TopBar } from '@/components/layout/top-bar'; import { useThemeTokens } from '@/providers/theme-token-provider'; import type { ThemeTokens } from '@/providers/theme-token-provider'; import { notify } from '@/lib/toast'; const THEME_API_URL = import.meta.env.VITE_THEME_API_URL ?? import.meta.env.VITE_API_URL ?? 'http://localhost:4100'; const COLOR_STOPS = ['25', '50', '100', '200', '300', '400', '500', '600', '700', '800', '900', '950']; const FONT_OPTIONS = [ { id: "'Satoshi', 'Inter', -apple-system, sans-serif", label: 'Satoshi' }, { id: "'General Sans', 'Inter', -apple-system, sans-serif", label: 'General Sans' }, { id: "'Inter', -apple-system, sans-serif", label: 'Inter' }, { id: "'DM Sans', 'Inter', sans-serif", label: 'DM Sans' }, { id: "'Plus Jakarta Sans', 'Inter', sans-serif", label: 'Plus Jakarta Sans' }, { id: "'Nunito Sans', 'Inter', sans-serif", label: 'Nunito Sans' }, { id: "'Source Sans 3', 'Inter', sans-serif", label: 'Source Sans 3' }, { id: "'Poppins', 'Inter', sans-serif", label: 'Poppins' }, { id: "'Lato', 'Inter', sans-serif", label: 'Lato' }, { id: "'Open Sans', 'Inter', sans-serif", label: 'Open Sans' }, { id: "'Roboto', 'Inter', sans-serif", label: 'Roboto' }, { id: "'Noto Sans', 'Inter', sans-serif", label: 'Noto Sans' }, ]; const COLOR_PRESETS: Record }> = { blue: { label: 'Blue', colors: { '25': 'rgb(239 246 255)', '50': 'rgb(219 234 254)', '100': 'rgb(191 219 254)', '200': 'rgb(147 197 253)', '300': 'rgb(96 165 250)', '400': 'rgb(59 130 246)', '500': 'rgb(37 99 235)', '600': 'rgb(29 78 216)', '700': 'rgb(30 64 175)', '800': 'rgb(30 58 138)', '900': 'rgb(23 37 84)', '950': 'rgb(15 23 42)' }, }, teal: { label: 'Teal', colors: { '25': 'rgb(240 253 250)', '50': 'rgb(204 251 241)', '100': 'rgb(153 246 228)', '200': 'rgb(94 234 212)', '300': 'rgb(45 212 191)', '400': 'rgb(20 184 166)', '500': 'rgb(13 148 136)', '600': 'rgb(15 118 110)', '700': 'rgb(17 94 89)', '800': 'rgb(19 78 74)', '900': 'rgb(17 63 61)', '950': 'rgb(4 47 46)' }, }, violet: { label: 'Violet', colors: { '25': 'rgb(250 245 255)', '50': 'rgb(245 235 255)', '100': 'rgb(235 215 254)', '200': 'rgb(214 187 251)', '300': 'rgb(182 146 246)', '400': 'rgb(158 119 237)', '500': 'rgb(127 86 217)', '600': 'rgb(105 65 198)', '700': 'rgb(83 56 158)', '800': 'rgb(66 48 125)', '900': 'rgb(53 40 100)', '950': 'rgb(44 28 95)' }, }, rose: { label: 'Rose', colors: { '25': 'rgb(255 241 242)', '50': 'rgb(255 228 230)', '100': 'rgb(254 205 211)', '200': 'rgb(253 164 175)', '300': 'rgb(251 113 133)', '400': 'rgb(244 63 94)', '500': 'rgb(225 29 72)', '600': 'rgb(190 18 60)', '700': 'rgb(159 18 57)', '800': 'rgb(136 19 55)', '900': 'rgb(112 26 53)', '950': 'rgb(76 5 25)' }, }, emerald: { label: 'Emerald', colors: { '25': 'rgb(236 253 245)', '50': 'rgb(209 250 229)', '100': 'rgb(167 243 208)', '200': 'rgb(110 231 183)', '300': 'rgb(52 211 153)', '400': 'rgb(16 185 129)', '500': 'rgb(5 150 105)', '600': 'rgb(4 120 87)', '700': 'rgb(6 95 70)', '800': 'rgb(6 78 59)', '900': 'rgb(6 62 48)', '950': 'rgb(2 44 34)' }, }, amber: { label: 'Amber', colors: { '25': 'rgb(255 251 235)', '50': 'rgb(254 243 199)', '100': 'rgb(253 230 138)', '200': 'rgb(252 211 77)', '300': 'rgb(251 191 36)', '400': 'rgb(245 158 11)', '500': 'rgb(217 119 6)', '600': 'rgb(180 83 9)', '700': 'rgb(146 64 14)', '800': 'rgb(120 53 15)', '900': 'rgb(99 49 18)', '950': 'rgb(69 26 3)' }, }, slate: { label: 'Slate', colors: { '25': 'rgb(248 250 252)', '50': 'rgb(241 245 249)', '100': 'rgb(226 232 240)', '200': 'rgb(203 213 225)', '300': 'rgb(148 163 184)', '400': 'rgb(100 116 139)', '500': 'rgb(71 85 105)', '600': 'rgb(47 64 89)', '700': 'rgb(37 49 72)', '800': 'rgb(30 41 59)', '900': 'rgb(15 23 42)', '950': 'rgb(2 6 23)' }, }, }; // Generate a full color scale from a single hex color const hexToHsl = (hex: string): [number, number, number] => { const r = parseInt(hex.slice(1, 3), 16) / 255; const g = parseInt(hex.slice(3, 5), 16) / 255; const b = parseInt(hex.slice(5, 7), 16) / 255; const max = Math.max(r, g, b), min = Math.min(r, g, b); let h = 0, s = 0; const l = (max + min) / 2; if (max !== min) { const d = max - min; s = l > 0.5 ? d / (2 - max - min) : d / (max + min); if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6; else if (max === g) h = ((b - r) / d + 2) / 6; else h = ((r - g) / d + 4) / 6; } return [h * 360, s * 100, l * 100]; }; const hslToRgb = (h: number, s: number, l: number): string => { h /= 360; s /= 100; l /= 100; let r: number, g: number, b: number; if (s === 0) { r = g = b = l; } else { const hue2rgb = (p: number, q: number, t: number) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1/2) return q; if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } return `rgb(${Math.round(r * 255)} ${Math.round(g * 255)} ${Math.round(b * 255)})`; }; const generatePalette = (hex: string): Record => { const [h, s] = hexToHsl(hex); // Map each stop to a lightness value const stops: Record = { '25': 97, '50': 94, '100': 89, '200': 80, '300': 68, '400': 56, '500': 46, '600': 38, '700': 31, '800': 26, '900': 20, '950': 13, }; const result: Record = {}; for (const [stop, lightness] of Object.entries(stops)) { // Desaturate lighter stops slightly for natural feel const satAdj = lightness > 80 ? s * 0.6 : lightness > 60 ? s * 0.85 : s; result[stop] = hslToRgb(h, satAdj, lightness); } return result; }; const rgbToHex = (rgb: string): string => { const match = rgb.match(/(\d+)\s+(\d+)\s+(\d+)/); if (!match) return '#3b82f6'; const [, r, g, b] = match; return `#${[r, g, b].map(c => parseInt(c).toString(16).padStart(2, '0')).join('')}`; }; const Section = ({ title, children }: { title: string; children: React.ReactNode }) => (

{title}

{children}
); const FileUploadField = ({ label, value, onChange, accept }: { label: string; value: string; onChange: (v: string) => void; accept: string }) => { const inputRef = useRef(null); const handleFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => onChange(reader.result as string); reader.readAsDataURL(file); }; return (
{label}
{value && {label}} {value && {value.startsWith('data:') ? 'Uploaded file' : value}}
); }; export const BrandingSettingsPage = () => { const { tokens, refresh } = useThemeTokens(); const [form, setForm] = useState(tokens); const [saving, setSaving] = useState(false); const [resetting, setResetting] = useState(false); useEffect(() => { setForm(tokens); }, [tokens]); const updateBrand = (key: keyof ThemeTokens['brand'], value: string) => { setForm(prev => ({ ...prev, brand: { ...prev.brand, [key]: value } })); }; const updateColor = (stop: string, value: string) => { setForm(prev => ({ ...prev, colors: { ...prev.colors, brand: { ...prev.colors.brand, [stop]: value } } })); }; const updateTypography = (key: keyof ThemeTokens['typography'], value: string) => { setForm(prev => ({ ...prev, typography: { ...prev.typography, [key]: value } })); }; const updateLogin = (key: string, value: any) => { if (key === 'poweredBy.label') { setForm(prev => ({ ...prev, login: { ...prev.login, poweredBy: { ...prev.login.poweredBy, label: value } } })); } else if (key === 'poweredBy.url') { setForm(prev => ({ ...prev, login: { ...prev.login, poweredBy: { ...prev.login.poweredBy, url: value } } })); } else { setForm(prev => ({ ...prev, login: { ...prev.login, [key]: value } })); } }; const updateSidebar = (key: keyof ThemeTokens['sidebar'], value: string) => { setForm(prev => ({ ...prev, sidebar: { ...prev.sidebar, [key]: value } })); }; const updateQuickAction = (index: number, key: 'label' | 'prompt', value: string) => { setForm(prev => { const actions = [...prev.ai.quickActions]; actions[index] = { ...actions[index], [key]: value }; return { ...prev, ai: { quickActions: actions } }; }); }; const addQuickAction = () => { setForm(prev => ({ ...prev, ai: { quickActions: [...prev.ai.quickActions, { label: '', prompt: '' }] }, })); }; const removeQuickAction = (index: number) => { setForm(prev => ({ ...prev, ai: { quickActions: prev.ai.quickActions.filter((_, i) => i !== index) }, })); }; const handleSave = async () => { setSaving(true); try { await fetch(`${THEME_API_URL}/api/config/theme`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) }); await refresh(); notify.success('Branding Saved', 'Theme updated successfully'); } catch { notify.error('Save Failed', 'Could not update theme'); } finally { setSaving(false); } }; const handleReset = async () => { setResetting(true); try { await fetch(`${THEME_API_URL}/api/config/theme/reset`, { method: 'POST' }); await refresh(); notify.success('Branding Reset', 'Theme restored to defaults'); } catch { notify.error('Reset Failed', 'Could not reset theme'); } finally { setResetting(false); } }; return ( <>
{/* LEFT COLUMN */}
{/* Brand Identity */}
updateBrand('name', v)} /> updateBrand('hospitalName', v)} />
updateBrand('logo', v)} accept="image/*" /> updateBrand('favicon', v)} accept="image/*,.ico" />
{/* Typography */}
{/* Login Page */}
updateLogin('title', v)} /> updateLogin('subtitle', v)} />
updateLogin('showGoogleSignIn', v)} label="Google Sign-in" /> updateLogin('showForgotPassword', v)} label="Forgot Password" />
updateLogin('poweredBy.label', v)} /> updateLogin('poweredBy.url', v)} />
{/* Sidebar */}
updateSidebar('title', v)} /> updateSidebar('subtitle', v)} hint="{role} = user role" />
{/* RIGHT COLUMN */}
{/* Brand Colors */}

Pick a base color or preset — the full palette generates automatically.

{/* Color picker */}
Base Color

{rgbToHex(form.colors.brand['500'] ?? 'rgb(37 99 235)')}

{/* Presets */}
{Object.entries(COLOR_PRESETS).map(([key, preset]) => ( ))}
{/* Generated palette preview */}
{COLOR_STOPS.map(stop => (
{stop}
))}
{/* AI Quick Actions */}
{form.ai.quickActions.map((action, i) => (
updateQuickAction(i, 'label', v)} /> updateQuickAction(i, 'prompt', v)} />
))}
{/* Footer — pinned */}
); };