From ccd4e23217acd52ad6dbc7ecf00e01e5a8898c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Sat, 29 Aug 2026 21:42:20 +0100 Subject: [PATCH] Home picks a gallery instead of managing one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Managing items on the page would be a second editor for something that already has one, and the two would drift — a caption fixed in one place and not the other. The page chooses; the Galleries section arranges; every page using that gallery changes with it. The list of galleries is read from the section that owns them rather than embedded in the home payload, because a copy there would be stale the moment somebody edited one. While no gallery is chosen the old per-page rows still render, and the panel says so — a carousel that appears to come from nowhere is worse than a sentence. Co-Authored-By: Claude Opus 5 --- web/sections/HomePanel.tsx | 155 ++++++++++++------------------------- 1 file changed, 51 insertions(+), 104 deletions(-) diff --git a/web/sections/HomePanel.tsx b/web/sections/HomePanel.tsx index 24acbbf..b37e6a6 100644 --- a/web/sections/HomePanel.tsx +++ b/web/sections/HomePanel.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { ArrowDown, ArrowUp, Check, FolderOpen, ImagePlus, Loader2, Trash2 } from 'lucide-react'; +import { Check, FolderOpen, Loader2 } from 'lucide-react'; import { useClient } from 'hooks/useClient'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -44,9 +44,14 @@ type GalleryItem = { type Home = { hero: { url: string | null; crop: string | null; fit: string | null }; intro_text: string | null; + /** The OLD per-page list. Still returned by the API, and only rendered while no gallery is chosen. */ gallery: GalleryItem[]; + /** Which gallery the carousel shows. Null means the old list above still applies. */ + gallery_id: number | null; }; +type Gallery = { id: number; name: string; slug: string; items: unknown[] }; + export const HomePanel = () => { const client = useClient(); const clientRef = useRef(client); @@ -55,11 +60,11 @@ export const HomePanel = () => { const [home, setHome] = useState(null); const [heroUrl, setHeroUrl] = useState(''); const [intro, setIntro] = useState(''); - const [addUrl, setAddUrl] = useState(''); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const [saved, flashSaved] = useFlash(); const [picking, setPicking] = useState(false); + const [galleries, setGalleries] = useState([]); /** Every write answers with the whole payload, so this is the only place state is set. */ const apply = useCallback((data: Home) => { @@ -81,6 +86,15 @@ export const HomePanel = () => { void load(); }, [load]); + // The galleries to choose between, read from the section that owns them rather than embedded in the + // home payload — a copy there would go stale the moment somebody edited one. + useEffect(() => { + void clientRef.current + .get<{ data: Gallery[] }>('/fofos-admin/galleries') + .then((response) => setGalleries(response.data)) + .catch(() => setGalleries([])); + }, []); + const send = async (label: string, call: () => Promise<{ data: Home }>) => { setBusy(label); setError(null); @@ -107,34 +121,6 @@ export const HomePanel = () => { const saveHero = () => send('hero', () => clientRef.current.patch<{ data: Home }>('/fofos-admin/home', { hero_url: heroUrl.trim() || null })); - const add = () => - send('add', async () => { - const response = await clientRef.current.post<{ data: Home }>('/fofos-admin/home/gallery', { - url: addUrl.trim(), - }); - setAddUrl(''); - return response; - }); - - const remove = (item: GalleryItem) => - send(`del:${item.id}`, () => clientRef.current.delete<{ data: Home }>(`/fofos-admin/home/gallery/${item.id}`)); - - const setAlt = (item: GalleryItem, alt: string) => - send(`alt:${item.id}`, () => - clientRef.current.patch<{ data: Home }>(`/fofos-admin/home/gallery/${item.id}`, { alt: alt.trim() || null }), - ); - - /** Moving one sends them ALL, which is what makes two items claiming a position impossible. */ - const move = (index: number, by: -1 | 1) => { - if (!home) return; - const next = [...home.gallery]; - const target = index + by; - if (target < 0 || target >= next.length) return; - [next[index], next[target]] = [next[target]!, next[index]!]; - return send('order', () => - clientRef.current.put<{ data: Home }>('/fofos-admin/home/gallery/order', { ids: next.map((item) => item.id) }), - ); - }; if (!home) { return ( @@ -233,87 +219,48 @@ export const HomePanel = () => {

Gallery

- The carousel below the intro. {home.gallery.length} item{home.gallery.length === 1 ? '' : 's'}, in order. + Which gallery the carousel shows. Edit its contents in the Galleries section — every page + using it changes with it.

-
    - {home.gallery.map((item, index) => ( -
  • - {index + 1} + {/* + A picker, not a list. - {/* Through the plugin's own proxy, so it loads with the session rather than needing the - dev server to be reachable from the browser. */} - {item.kind === 'photo' ? ( - - ) : ( - - {item.kind} - - )} - - - {item.url} - { - if (event.target.value.trim() !== (item.alt ?? '')) void setAlt(item, event.target.value); - }} - placeholder="Describe it, for screen readers" - className="h-7 text-xs" - /> - - - - - - - -
  • + Managing items here would be a second editor for something that already has one, and the two + would drift: a caption fixed in one place and not the other. The page chooses; the Galleries + section arranges. + */} + -
    - setAddUrl(event.target.value)} - placeholder="/media/…" - className="min-w-0 flex-1 font-mono text-xs" - autoComplete="off" - spellCheck={false} - /> - -
    + {/* Only while it is true: the old rows are what renders until a gallery is chosen, and saying so + beats a carousel that appears to come from nowhere. */} + {home.gallery_id === null && home.gallery.length > 0 && ( +

    + Showing {home.gallery.length} item{home.gallery.length === 1 ? '' : 's'} from the old + per-page list. Choosing a gallery replaces it. +

    + )} + ); };