import { useState, useEffect, useRef } from 'react'; import { api } from '../utils/api.js'; import { useToast } from '../contexts/ToastContext.jsx'; const DEFAULT_TITLE_COLOR = '#1a73e8'; const DEFAULT_PUBLIC_COLOR = '#1a73e8'; const DEFAULT_DM_COLOR = '#a142f4'; const COLOUR_SUGGESTIONS = [ '#1a73e8', '#a142f4', '#e53935', '#fa7b17', '#fdd835', '#34a853', ]; // A single colour picker card: shows suggestions by default, Custom button opens native picker function ColourPicker({ label, value, onChange, preview }) { const [mode, setMode] = useState('suggestions'); // 'suggestions' | 'custom' const inputRef = useRef(null); // Auto-close custom mode as soon as a new colour is committed const handleCustomChange = (e) => { onChange(e.target.value); // 'change' fires on close of native picker on most platforms; // we also listen to 'input' for live preview but only close on 'change' }; const handleCustomInput = (e) => { onChange(e.target.value); }; const handleCustomChangeClose = (e) => { onChange(e.target.value); setMode('suggestions'); }; return (
{label}
{/* Current colour preview */}
{preview ? preview(value) :
} {value}
{mode === 'suggestions' && ( <>
{COLOUR_SUGGESTIONS.map(hex => (
)} {mode === 'custom' && (
{/* Colour input rendered as a large clickable swatch — works on mobile and desktop */}
)}
); } export default function BrandingModal({ onClose }) { const toast = useToast(); const [tab, setTab] = useState('general'); // 'general' | 'colours' const [settings, setSettings] = useState({}); const [appName, setAppName] = useState(''); const [loading, setLoading] = useState(false); const [resetting, setResetting] = useState(false); const [showResetConfirm, setShowResetConfirm] = useState(false); const [colourTitle, setColourTitle] = useState(DEFAULT_TITLE_COLOR); const [colourPublic, setColourPublic] = useState(DEFAULT_PUBLIC_COLOR); const [colourDm, setColourDm] = useState(DEFAULT_DM_COLOR); const [savingColours, setSavingColours] = useState(false); useEffect(() => { api.getSettings().then(({ settings }) => { setSettings(settings); setAppName(settings.app_name || 'jama'); setColourTitle(settings.color_title || DEFAULT_TITLE_COLOR); setColourPublic(settings.color_avatar_public || DEFAULT_PUBLIC_COLOR); setColourDm(settings.color_avatar_dm || DEFAULT_DM_COLOR); }).catch(() => {}); }, []); const notifySidebarRefresh = () => window.dispatchEvent(new Event('jama:settings-changed')); const handleSaveName = async () => { if (!appName.trim()) return; setLoading(true); try { await api.updateAppName(appName.trim()); setSettings(prev => ({ ...prev, app_name: appName.trim() })); toast('App name updated', 'success'); notifySidebarRefresh(); } catch (e) { toast(e.message, 'error'); } finally { setLoading(false); } }; const handleLogoUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; if (file.size > 1024 * 1024) return toast('Logo must be less than 1MB', 'error'); try { const { logoUrl } = await api.uploadLogo(file); setSettings(prev => ({ ...prev, logo_url: logoUrl })); toast('Logo updated', 'success'); notifySidebarRefresh(); } catch (e) { toast(e.message, 'error'); } }; const handleSaveColours = async () => { setSavingColours(true); try { await api.updateColors({ colorTitle: colourTitle, colorAvatarPublic: colourPublic, colorAvatarDm: colourDm, }); setSettings(prev => ({ ...prev, color_title: colourTitle, color_avatar_public: colourPublic, color_avatar_dm: colourDm, })); toast('Colours updated', 'success'); notifySidebarRefresh(); } catch (e) { toast(e.message, 'error'); } finally { setSavingColours(false); } }; const handleReset = async () => { setResetting(true); try { await api.resetSettings(); const { settings: fresh } = await api.getSettings(); setSettings(fresh); setAppName(fresh.app_name || 'jama'); setColourTitle(DEFAULT_TITLE_COLOR); setColourPublic(DEFAULT_PUBLIC_COLOR); setColourDm(DEFAULT_DM_COLOR); toast('Settings reset to defaults', 'success'); notifySidebarRefresh(); setShowResetConfirm(false); } catch (e) { toast(e.message, 'error'); } finally { setResetting(false); } }; return (
e.target === e.currentTarget && onClose()}>

Branding

{/* Tabs */}
{tab === 'general' && ( <> {/* App Logo */}
App Logo
logo

Square format, max 1MB. Used in sidebar, login page and browser tab.

{/* App Name */}
App Name
setAppName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSaveName()} />
{/* Reset */}
Reset
{!showResetConfirm ? ( ) : (

This will reset the app name, logo and all colours to their install defaults. This cannot be undone.

)} {settings.app_version && ( v{settings.app_version} )}
{settings.pw_reset_active === 'true' && (
⚠️ ADMPW_RESET is active. The default admin password is being reset on every restart. Set ADMPW_RESET=false in your environment variables to stop this.
)} )} {tab === 'colours' && (
(
A
)} />
(
B
)} />
)}
); }