diff --git a/web/PublishButton.tsx b/web/PublishButton.tsx index 57cc704..9fd10d3 100644 --- a/web/PublishButton.tsx +++ b/web/PublishButton.tsx @@ -1,5 +1,5 @@ import { useRef, useState } from 'react'; -import { Check, CircleAlert, Loader2, Upload } from 'lucide-react'; +import { Check, CircleAlert, Hammer, Loader2, Upload } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { readableError } from './errors'; @@ -7,94 +7,143 @@ import { useFlash } from './useFlash'; import { useReady } from './useReady'; /** - * Publish, from wherever you happen to be. + * Publish and Build, from wherever you happen to be. * - * ── Why it is in the header and not only in Administration ── + * ── Why they are 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. + * panel 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. + * They are the same actions Administration's card offers. Two entry points to one thing is right here: + * that card belongs to a row about the production server and reports what a build produced, while + * these are the verbs at the end of a sentence somebody has just written. * - * ── It takes minutes, so it says so ── + * ── Why both, when Publish already builds ── * - * 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. + * They answer different questions, and the difference is not obvious from the words: * - * ── Failure has to be readable, and there is no room for it ── + * Publish copies what you edited in DEVELOPMENT over production, then renders the site. + * Build renders production as it already is. Nothing moves. * - * 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. + * Publish is the one to press after editing anything. Build is for when the content is already + * promoted and only the code changed — a template fix, a new rendition ladder, a failed build to + * retry. Offering only Publish would mean every retry of a build also re-copied a database and took + * another backup, which is a lot of machinery for "try that again". + * + * ── One busy state, one failure panel ── + * + * Only one of these can be running, so they share both. A header is one line, and a failure here is + * usually a build error — long, and worth reading — so it opens a panel underneath rather than + * replacing a label. Positioned absolutely so nothing shifts, and it stays until dismissed: a message + * that vanishes on a timer is one somebody was still reading. */ +type Action = 'publish' | 'build'; + +const ROUTES: Record = { + publish: '/fofos-admin/_config/static/publish', + build: '/fofos-admin/_config/static/build', +}; + +const LABELS: Record = { + publish: { + idle: 'Publish', + busy: 'Publishing…', + done: 'Published', + title: 'Copy what you have edited into production, then rebuild the site', + }, + build: { + idle: 'Build', + busy: 'Building…', + done: 'Built', + title: 'Rebuild the site from what production already holds. Nothing is copied.', + }, +}; + export const PublishButton = () => { const client = useClient(); const clientRef = useRef(client); clientRef.current = client; const { ready } = useReady(); - const [busy, setBusy] = useState(false); + const [busy, setBusy] = useState(null); const [failure, setFailure] = useState(null); const [done, flashDone] = useFlash(6000); + const [finished, setFinished] = useState(null); - const publish = async () => { - setBusy(true); + const run = async (action: Action) => { + setBusy(action); 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. + const result = await clientRef.current.post<{ ok: boolean; output: string }>(ROUTES[action], {}); + // `ok: false` is a REFUSAL the server explains — a backup that could not be taken, a build that + // failed. It answers 400 with the reason, which the client throws on, so this is the belt to + // that braces rather than the usual path. if (!result.ok) setFailure(result.output || 'It did not say why.'); - else flashDone(); + else { + setFinished(action); + flashDone(); + } } catch (caught) { setFailure(readableError(caught)); } finally { - setBusy(false); + setBusy(null); } }; + const label = (action: Action) => { + const words = LABELS[action]; + if (busy === action) return words.busy; + if (done && finished === action) return words.done; + return words.idle; + }; + + const icon = (action: Action) => { + if (busy === action) return ; + if (done && finished === action) return ; + if (action === 'publish') return ; + return ; + }; + return ( -
+
+ {failure && ( + + )} + + {/* Build first and quieter. Publish is the one to press, so it reads last and solid — the row + runs left to right towards the action somebody actually came for. */} + + {failure && ( /* Absolute, so the header does not grow and push the panel down while somebody reads it. */
-

Publish failed

+

That did not work

             {failure}