import { useRef, useState } from 'react'; import { Check, CircleAlert, Loader2, Upload } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { readableError } from './errors'; import { useFlash } from './useFlash'; import { useReady } from './useReady'; /** * Publish, from wherever you happen to be. * * ── Why it is in the header and not only in Administration ── * * Publishing is the last step of every edit — write something on Home, choose a gallery, add a drop, * then make it public. It lived in one card at the bottom of one section, so finishing a change meant * leaving the thing you were working on to go and find the button. A person who does not know the * panel well simply does not find it, and concludes their edit went live when it did not. * * It is the same action Administration's card offers. Two entry points to one thing is right here: * that card belongs to a row about the production server and shows what a build produced, while this * is the verb at the end of a sentence you have just written. * * ── It takes minutes, so it says so ── * * The whole site is re-rendered. A button that greys out and does nothing visible for three minutes * reads as broken, and the second press is somebody assuming the first did not register — which is why * the label changes rather than only the cursor. * * ── Failure has to be readable, and there is no room for it ── * * A header is one line. So a failure opens a panel underneath it, positioned absolutely so nothing * shifts, holding the server's own words — usually a build error, which is long and worth reading. It * stays until dismissed: a message that vanishes on a timer is one somebody was still reading. */ export const PublishButton = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const { ready } = useReady(); const [busy, setBusy] = useState(false); const [failure, setFailure] = useState(null); const [done, flashDone] = useFlash(6000); const publish = async () => { setBusy(true); setFailure(null); try { const result = await clientRef.current.post<{ ok: boolean; output: string }>( '/fofos-admin/_config/static/publish', {}, ); // `ok: false` is a REFUSAL the server explains, not an exception — a backup that could not be // taken, a build that failed. It answers 400 with the reason, which the client throws on, so // this branch is the belt to that braces. if (!result.ok) setFailure(result.output || 'It did not say why.'); else flashDone(); } catch (caught) { setFailure(readableError(caught)); } finally { setBusy(false); } }; return (
{failure && ( /* Absolute, so the header does not grow and push the panel down while somebody reads it. */

Publish failed

            {failure}
          
)}
); };