import { useState, useEffect, useRef } from "react"; import { useBranding } from "../BrandingContext.js"; interface SettingsForm { businessName: string; primaryColor: string; accentColor: string; logoBase64: string | null; logoMimeType: string | null; } export function SettingsPage() { const { refresh } = useBranding(); const [form, setForm] = useState({ businessName: "", primaryColor: "#4f8a6f", accentColor: "#8b7355", logoBase64: null, logoMimeType: null, }); const [saving, setSaving] = useState(false); const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); const [loaded, setLoaded] = useState(false); const fileInputRef = useRef(null); useEffect(() => { fetch("/api/admin/settings") .then((r) => r.json()) .then((data) => { setForm({ businessName: data.businessName ?? "GroomBook", primaryColor: data.primaryColor ?? "#4f8a6f", accentColor: data.accentColor ?? "#8b7355", logoBase64: data.logoBase64 ?? null, logoMimeType: data.logoMimeType ?? null, }); setLoaded(true); }) .catch(() => setLoaded(true)); }, []); const handleLogoChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 512 * 1024) { setMessage({ type: "error", text: "Logo must be under 512KB." }); return; } const validTypes = ["image/png", "image/svg+xml", "image/jpeg", "image/webp"]; if (!validTypes.includes(file.type)) { setMessage({ type: "error", text: "Logo must be PNG, SVG, JPEG, or WebP." }); return; } const reader = new FileReader(); reader.onload = () => { const result = reader.result as string; // Strip the data:...;base64, prefix const base64 = result.split(",")[1] ?? null; setForm((f) => ({ ...f, logoBase64: base64, logoMimeType: file.type as SettingsForm["logoMimeType"] })); setMessage(null); }; reader.readAsDataURL(file); }; const handleSave = async () => { setSaving(true); setMessage(null); try { const res = await fetch("/api/admin/settings", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(form), }); if (!res.ok) { const err = await res.json().catch(() => null); throw new Error(err?.error ?? "Failed to save settings"); } setMessage({ type: "success", text: "Settings saved." }); refresh(); } catch (err: unknown) { setMessage({ type: "error", text: err instanceof Error ? err.message : "Save failed" }); } finally { setSaving(false); } }; if (!loaded) return

Loading settings...

; const logoSrc = form.logoBase64 && form.logoMimeType ? `data:${form.logoMimeType};base64,${form.logoBase64}` : null; return (

Branding & Appearance

Customize your business name, logo, and color scheme.

{/* Business Name */}
setForm((f) => ({ ...f, businessName: e.target.value }))} style={{ width: "100%", padding: "0.5rem 0.75rem", border: "1px solid #d1d5db", borderRadius: 6, fontSize: 14, }} />
{/* Logo Upload */}
{logoSrc ? ( Logo preview ) : (
No logo
)}
{logoSrc && ( )}

PNG, SVG, JPEG, or WebP. Max 512KB.

{/* Color Pickers */}
setForm((f) => ({ ...f, primaryColor: e.target.value }))} style={{ width: 40, height: 40, border: "none", cursor: "pointer" }} /> setForm((f) => ({ ...f, primaryColor: e.target.value }))} style={{ width: 90, padding: "0.4rem 0.5rem", border: "1px solid #d1d5db", borderRadius: 6, fontSize: 13, fontFamily: "monospace", }} />
setForm((f) => ({ ...f, accentColor: e.target.value }))} style={{ width: 40, height: 40, border: "none", cursor: "pointer" }} /> setForm((f) => ({ ...f, accentColor: e.target.value }))} style={{ width: 90, padding: "0.4rem 0.5rem", border: "1px solid #d1d5db", borderRadius: 6, fontSize: 13, fontFamily: "monospace", }} />
{/* Preview */}

Preview

{logoSrc && ( )} {form.businessName} Button Accent
{/* Save */} {message && (
{message.text}
)}
); }