'use client'; import { useEffect, useRef, useState } from 'react'; import { Save, Loader2, RotateCcw, ImageIcon, Upload, Trash2 } from 'lucide-react'; import { apiFetch } from '@/lib/browser-navigation'; interface ConfigEntry { value: unknown; source: 'admin' | 'env' | 'default'; } const IMAGE_FIELDS = [ { key: 'faviconUrl', label: 'Favicon', accept: '.svg,.png,.ico,.webp' }, { key: 'appLogoLightUrl', label: 'App Logo (Light Mode)', accept: '.svg,.png,.jpg,.webp' }, { key: 'appLogoDarkUrl', label: 'App Logo (Dark Mode)', accept: '.svg,.png,.jpg,.webp' }, { key: 'loginLogoLightUrl', label: 'Login Logo (Light Mode)', accept: '.svg,.png,.jpg,.webp' }, { key: 'loginLogoDarkUrl', label: 'Login Logo (Dark Mode)', accept: '.svg,.png,.jpg,.webp' }, ]; const TEXT_FIELDS = [ { key: 'loginCompanyName', label: 'Company Name' }, { key: 'loginImprintUrl', label: 'Imprint URL' }, { key: 'loginPrivacyPolicyUrl', label: 'Privacy Policy URL' }, { key: 'loginWebsiteUrl', label: 'Company Website URL' }, ]; export default function AdminBrandingPage() { const [config, setConfig] = useState>({}); const [edits, setEdits] = useState>({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [uploading, setUploading] = useState(null); const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const fileInputRefs = useRef>({}); useEffect(() => { fetchConfig(); }, []); async function fetchConfig() { setLoading(true); const res = await apiFetch('/api/admin/config'); if (res.ok) setConfig(await res.json()); setLoading(false); } function handleChange(key: string, value: string) { setEdits(prev => ({ ...prev, [key]: value })); setMessage(null); } function currentValue(key: string): string { if (key in edits) return edits[key] as string; return (config[key]?.value as string) ?? ''; } async function handleSave() { if (Object.keys(edits).length === 0) return; setSaving(true); setMessage(null); const res = await apiFetch('/api/admin/config', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(edits), }); if (res.ok) { setMessage({ type: 'success', text: 'Branding updated. Changes visible on next page load.' }); setEdits({}); await fetchConfig(); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Failed to save' }); } setSaving(false); } async function handleUpload(slot: string, file: File) { setUploading(slot); setMessage(null); const formData = new FormData(); formData.append('file', file); formData.append('slot', slot); const res = await apiFetch('/api/admin/branding', { method: 'POST', body: formData, }); if (res.ok) { const data = await res.json(); setMessage({ type: 'success', text: `Uploaded ${file.name} successfully.` }); // Remove any pending URL edit for this slot since upload sets it setEdits(prev => { const next = { ...prev }; delete next[slot]; return next; }); // Update config to reflect the uploaded URL setConfig(prev => ({ ...prev, [slot]: { value: data.url, source: 'admin' }, })); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Upload failed' }); } setUploading(null); } async function handleDeleteUpload(slot: string) { setMessage(null); const res = await apiFetch('/api/admin/branding', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slot }), }); if (res.ok) { setMessage({ type: 'success', text: 'Uploaded file removed. Reverted to default.' }); setEdits(prev => { const next = { ...prev }; delete next[slot]; return next; }); await fetchConfig(); } else { const data = await res.json(); setMessage({ type: 'error', text: data.error || 'Failed to remove' }); } } async function handleRevert(key: string) { const res = await apiFetch('/api/admin/config', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key }), }); if (res.ok) { setEdits(prev => { const next = { ...prev }; delete next[key]; return next; }); await fetchConfig(); } } const isUploadedFile = (key: string): boolean => { const val = currentValue(key); return val.startsWith('/api/admin/branding/'); }; const hasEdits = Object.keys(edits).length > 0; if (loading) { return
Loading...
; } return (

Branding

Customize logos, favicon, and company information

{hasEdits && ( )}
{message && (
{message.text}
)}

Images & Logos

Upload a file or enter a URL. Supported formats: SVG, PNG, JPEG, WebP, ICO (max 2 MB)

{IMAGE_FIELDS.map(field => (
{config[field.key]?.source === 'admin' && ( {isUploadedFile(field.key) ? 'uploaded' : 'admin'} )}
handleChange(field.key, e.target.value)} placeholder="Enter URL or upload a file" className="h-8 w-full sm:w-64 min-w-0 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" /> { fileInputRefs.current[field.key] = el; }} type="file" accept={field.accept} className="hidden" onChange={(e) => { const file = e.target.files?.[0]; if (file) handleUpload(field.key, file); e.target.value = ''; }} /> {isUploadedFile(field.key) && ( )} {config[field.key]?.source === 'admin' && !isUploadedFile(field.key) && ( )}
{/* Preview */} {currentValue(field.key) && (
{field.label} { (e.target as HTMLImageElement).style.display = 'none'; }} />
)}
))}

Company Information

{TEXT_FIELDS.map(field => (
{config[field.key]?.source === 'admin' && ( admin )}
handleChange(field.key, e.target.value)} placeholder={field.key.includes('Url') ? 'https://...' : 'Enter value'} className="h-8 w-full sm:w-72 min-w-0 rounded-md border border-input bg-background px-2.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" /> {config[field.key]?.source === 'admin' && ( )}
))}
); }