import { useRef, useState } from 'react' import { api } from '../../../api/client.js' import { useSite } from '../../../contexts/SiteContext.jsx' // Admin · Appearance → Brand assets (docs/website/THEMING_AND_NAV.md §6.3). // // Three slots, each an override layer over the matching BRAND_* env value. An // empty slot is not "no image" — it is "whatever this instance was deployed // with", which is why every row shows what it currently resolves to rather than // an empty box. // // Unlike the theme form above, an upload SAVES IMMEDIATELY: the file and the // settings row are written by one request, because an upload that stored a file // and then waited for a Save press would leave litter in /uploads whenever the // admin changed their mind. Clearing a slot is the same deal in reverse. const SLOTS = [ { id: 'logo', label: 'Logo', accept: 'image/png,image/jpeg,image/webp,image/avif,image/gif', limit: '1 MB', envVar: 'BRAND_LOGO', help: 'Shown beside the moon in the site header, the admin sidebar and the player portal, and used as the link preview image when a page is shared.', }, { id: 'hero', label: 'Hero image', accept: 'image/png,image/jpeg,image/webp,image/avif,image/gif', limit: '8 MB', envVar: 'BRAND_HERO', // §4.9: the hero editor's own background beats this, and an admin who does // not know that files a bug against a working system. help: 'The image behind the portal hero. If the hero editor has its own background image set, that wins over this one.', }, { id: 'favicon', label: 'Favicon', accept: 'image/png', limit: '512 KB', envVar: 'BRAND_FAVICON', // §4.10: .ico would mean adding a type to the upload allowlist, and the // stored extension coming from that allowlist is what makes uploads safe. help: 'The browser tab icon. PNG only — a 32×32 or 64×64 square works everywhere.', }, ] export default function BrandAssetsPanel({ initial }) { const { brand, refresh: refreshSite } = useSite() const [assets, setAssets] = useState(initial || {}) const [busySlot, setBusySlot] = useState('') const [error, setError] = useState('') const inputs = useRef({}) async function upload(slot, file) { if (!file) return setBusySlot(slot) setError('') try { const res = await api.admin.uploadBrandAsset(slot, file) setAssets(res.brand_assets || {}) await refreshSite() } catch (err) { setError(err.message || 'Could not upload that image.') } finally { setBusySlot('') // Let the same file be picked again after a failure — a file input holds // its value, so re-choosing it would fire no change event. if (inputs.current[slot]) inputs.current[slot].value = '' } } async function clear(slot) { setBusySlot(slot) setError('') try { const next = { ...assets } delete next[slot] // Clearing the last override deletes the row rather than storing `{}` — // absence of the row is what selects the env defaults (§2), and a stored // empty object would be a different state that means the same thing. if (Object.keys(next).length) await api.admin.updateSettings({ brand_assets: next }) else await api.admin.resetSetting('brand_assets') setAssets(next) await refreshSite() } catch (err) { setError(err.message || 'Could not clear that asset.') } finally { setBusySlot('') } } return (
{assets[slot.id]}
>
) : effective ? (
<>
Using the deployed default from {slot.envVar}
>
) : (
<>
Not set — {slot.envVar} is empty, so nothing is rendered
>
)}