import { useState, useEffect, useRef, useCallback } from 'react';
import { api } from '../utils/api.js';
import { useToast } from '../contexts/ToastContext.jsx';
const DEFAULT_TITLE_COLOR = '#1a73e8'; // light mode default
const DEFAULT_TITLE_DARK_COLOR = '#60a5fa'; // dark mode default (lighter blue readable on dark bg)
const DEFAULT_PUBLIC_COLOR = '#1a73e8';
const DEFAULT_DM_COLOR = '#a142f4';
const COLOUR_SUGGESTIONS = [
'#1a73e8', '#a142f4', '#e53935', '#fa7b17', '#fdd835', '#34a853',
];
// ── Title Colour Row — one row per mode ──────────────────────────────────────
function TitleColourRow({ bgColor, bgLabel, textColor, onChange }) {
const [mode, setMode] = useState('idle'); // 'idle' | 'custom'
return (
{ dragging.current = true; handle(e); }}
onMouseMove={e => { if (dragging.current) handle(e); }}
onMouseUp={() => { dragging.current = false; }}
onMouseLeave={() => { dragging.current = false; }}
onTouchStart={handle} onTouchMove={handle} />
);
}
// ── Custom HSV picker ─────────────────────────────────────────────────────────
function CustomPicker({ initial, onSet, onBack }) {
const { h: ih, s: is, v: iv } = hexToHsv(initial);
const [hue, setHue] = useState(ih);
const [sat, setSat] = useState(is);
const [val, setVal] = useState(iv);
const [hexInput, setHexInput] = useState(initial);
const [hexError, setHexError] = useState(false);
const current = hsvToHex(hue, sat, val);
// Sync hex input when sliders change
useEffect(() => { setHexInput(current); setHexError(false); }, [current]);
const handleHexInput = (e) => {
const v = e.target.value;
setHexInput(v);
if (isValidHex(v)) {
const { h, s, v: bv } = hexToHsv(v);
setHue(h); setSat(s); setVal(bv);
setHexError(false);
} else {
setHexError(true);
}
};
return (
{ setSat(s); setVal(v); }} />
{/* Preview + hex input */}
{/* Actions */}
);
}
// ── ColourPicker card ─────────────────────────────────────────────────────────
function ColourPicker({ label, value, onChange, preview }) {
const [mode, setMode] = useState('suggestions'); // 'suggestions' | 'custom'
return (
{label}
{/* Current colour preview */}
{preview
? preview(value)
:
}
{value}
{mode === 'suggestions' && (
<>
{COLOUR_SUGGESTIONS.map(hex => (
>
)}
{mode === 'custom' && (
{ onChange(hex); setMode('suggestions'); }}
onBack={() => setMode('suggestions')} />
)}
);
}
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 [colourTitleDark, setColourTitleDark] = useState(DEFAULT_TITLE_DARK_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 || 'rosterchirp');
setColourTitle(settings.color_title || DEFAULT_TITLE_COLOR);
setColourTitleDark(settings.color_title_dark || DEFAULT_TITLE_DARK_COLOR);
setColourPublic(settings.color_avatar_public || DEFAULT_PUBLIC_COLOR);
setColourDm(settings.color_avatar_dm || DEFAULT_DM_COLOR);
}).catch(() => {});
}, []);
const notifySidebarRefresh = () => window.dispatchEvent(new Event('rosterchirp: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,
colorTitleDark: colourTitleDark,
colorAvatarPublic: colourPublic,
colorAvatarDm: colourDm,
});
setSettings(prev => ({
...prev,
color_title: colourTitle,
color_title_dark: colourTitleDark,
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 || 'rosterchirp');
setColourTitle(DEFAULT_TITLE_COLOR);
setColourTitleDark(DEFAULT_TITLE_DARK_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 Name */}
App Name
setAppName(e.target.value)} autoComplete="new-password" onKeyDown={e => e.key === 'Enter' && handleSaveName()} />
Maximum 16 characters including spaces. Currently {appName.length}/16.
{/* 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' && (
)}
);
}