From 28798ad67be6db14c6531d49f71c1bbe9423b6e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sun, 30 Aug 2026 10:22:05 +0100 Subject: [PATCH] Build sits beside Publish in the header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They answer different questions, and the difference is not obvious from the words: Publish copies what you edited in development over production, then renders the site. Build renders production as it already is. Nothing moves. 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". Build reads first and quieter, Publish last and solid: the row runs towards the action somebody actually came for. One busy state and one failure panel between them, because only one can be running. Co-Authored-By: Claude Opus 5 (1M context) --- web/PublishButton.tsx | 147 ++++++++++++++++++++++++++++-------------- 1 file changed, 98 insertions(+), 49 deletions(-) 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}