import { useEffect, useRef, useState } from 'react'; import { Input } from '@/components/ui/input'; import { useClient } from 'hooks/useClient'; import { readableError } from '../errors'; import { useAutosave } from '../useAutosave'; import { useFlash } from '../useFlash'; import { useDevice } from '../DeviceToggle'; /** * A block's own heading. Optional, and above whatever editor the block has. * * ── Why it is not inside the free text ── * * Typing a heading into the text works for one kind of block and no others. A gallery, a video and a * partner row all want to announce themselves and have nowhere to put the words — so the heading is a * property of the BLOCK rather than of its contents, and every kind gets one. * * That is also why this is announced for every selected block, including the ones with no editor of * their own: `canEditComponent` answers "is there content to type", and a heading is not content. */ export const BlockSettings = ({ pageId, sectionId, block, steps, onSaved, }: { pageId: number; sectionId: number; block: { id: number; title?: string | null; padding?: Record }; /** The padding scale, sent by the API so this file holds no copy of it. */ steps: number[]; onSaved: () => void; }) => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const [device] = useDevice(); const breakpoint = ({ desktop: 'lg', tablet: 'md', mobile: 'base' } as const)[device]; const [draft, setDraft] = useState({ title: block.title ?? '', padding: block.padding ?? {} }); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const [saved, flashSaved] = useFlash(); // Keyed on the id, so re-reading the same block after a save does not throw away what is being typed. useEffect(() => setDraft({ title: block.title ?? '', padding: block.padding ?? {} }), [block.id]); /** What an axis falls back to at this width: the nearest smaller one that has a value. */ const inherited = (axis: 'x' | 'y'): number | null => { const below = { base: [], md: ['base'], lg: ['md', 'base'] } as const; for (const smaller of below[breakpoint]) { const step = draft.padding[smaller]?.[axis]; if (typeof step === 'number') return step; } return null; }; const setPadding = (axis: 'x' | 'y', value: string) => { const at = { ...(draft.padding[breakpoint] ?? {}) }; if (value === '') delete at[axis]; else at[axis] = Number(value); const next = { ...draft.padding }; // An empty breakpoint is removed rather than kept as `{}`, so "inherit everything" stays an // absence instead of a thing that has to be told apart from one later. if (Object.keys(at).length) next[breakpoint] = at; else delete next[breakpoint]; setDraft((was) => ({ ...was, padding: next })); }; const save = async () => { setBusy(true); setError(null); try { // Empty is sent as an empty string, which the API reads as "take the heading off". await clientRef.current.patch( `/fofos-admin/pages/${pageId}/sections/${sectionId}/components/${block.id}`, { title: draft.title, padding: draft.padding }, ); flashSaved(); onSaved(); } catch (failure) { setError(readableError(failure)); } finally { setBusy(false); } }; const stored = JSON.stringify({ title: block.title ?? '', padding: block.padding ?? {} }); const pending = JSON.stringify(draft); useAutosave(pending !== stored ? pending : null, busy, save); return (
{ // Read NOW — see the note in BandSettings. A functional updater runs after the handler has // returned, and `currentTarget` is null by then. const value = event.currentTarget.value; setDraft((was) => ({ ...was, title: value })); }} /> {/* The block's own spacing, at the width the toggle above is showing. It had a Spacing link in the chrome painted onto the page, which went with the rest of it. It belongs here: it is a property of the selected block, like the heading, and one of the two is not worth a separate place to go. Unset inherits the smaller width, and 0 is the other instruction — none HERE even though the phone has some. So they are separate options. */}
{( [ ['x', 'Horizontal padding'], ['y', 'Vertical padding'], ] as const ).map(([axis, label]) => { const own = draft.padding[breakpoint]?.[axis]; const from = inherited(axis); return (
); })}
{saved &&

Saved

} {error &&

{error}

}
); };