finish the carousel rename in the panels

Two of these were live: PageBuilderPanel still compared kind === 'gallery' in
two places, so a carousel block's editor and its Choose button were both dead
branches. The type checker could not see them — both compare against string.

The rest is copy somebody reads: "Change gallery", "Name a new gallery",
"Reading the gallery", "Add to this gallery", Home's Gallery heading, and the
"Create Gallery" file action, now "Carousel from folder".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 18:06:06 +01:00
co-authored by Claude Opus 5
parent 15042ccc76
commit 6e33e89e2c
15 changed files with 104 additions and 104 deletions
+19 -19
View File
@@ -10,7 +10,7 @@ import { MediaPicker } from '../MediaPicker';
import { useAutosave } from '../useAutosave';
/**
* The Home page: a hero, an intro, and an ordered gallery.
* The Home page: a hero, an intro, and an ordered carousel.
*
* ── It edits DEVELOPMENT, and that is the whole model ──
*
@@ -21,18 +21,18 @@ import { useAutosave } from '../useAutosave';
*
* ── The server owns the shape, and every write returns it ──
*
* `PATCH /home` and every gallery route answer with the WHOLE payload, so this never patches its own
* `PATCH /home` and every carousel route answer with the WHOLE payload, so this never patches its own
* state from what it sent. That matters more than it looks: the server clears the hero crop when the
* hero changes, and a client that kept its own copy would go on showing a framing the server had
* already discarded.
*
* ── Ordering is a list, not a field ──
*
* `PUT /gallery/order` takes every id in the order they should be. So moving one item sends all of
* `PUT /carousel/order` takes every id in the order they should be. So moving one item sends all of
* them, which is what makes it impossible to end up with two things claiming the same position.
*/
type GalleryItem = {
type CarouselItem = {
id: number;
kind: string;
url: string;
@@ -45,13 +45,13 @@ 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. */
/** The OLD per-page list. Still returned by the API, and only rendered while no carousel is chosen. */
carousel: CarouselItem[];
/** Which carousel the carousel shows. Null means the old list above still applies. */
carousel_id: number | null;
};
type Gallery = { id: number; name: string; slug: string; items: unknown[] };
type Carousel = { id: number; name: string; slug: string; items: unknown[] };
export const HomePanel = () => {
const client = useClient();
@@ -65,7 +65,7 @@ export const HomePanel = () => {
const [error, setError] = useState<string | null>(null);
const [saved, flashSaved] = useFlash();
const [picking, setPicking] = useState(false);
const [carousels, setCarousels] = useState<Gallery[]>([]);
const [carousels, setCarousels] = useState<Carousel[]>([]);
/** Every write answers with the whole payload, so this is the only place state is set. */
const apply = useCallback((data: Home) => {
@@ -91,7 +91,7 @@ export const HomePanel = () => {
// home payload — a copy there would go stale the moment somebody edited one.
useEffect(() => {
void clientRef.current
.get<{ data: Gallery[] }>('/fofos-admin/carousels')
.get<{ data: Carousel[] }>('/fofos-admin/carousels')
.then((response) => setCarousels(response.data))
.catch(() => setCarousels([]));
}, []);
@@ -119,7 +119,7 @@ export const HomePanel = () => {
/*
One Save for everything this page holds in a field.
The gallery choice is NOT part of it: picking a gallery from the list writes immediately, because a
The carousel choice is NOT part of it: picking a carousel from the list writes immediately, because a
<select> that needs a second press to mean anything is a control people believe they have already
used. The two fields below are typed, and typing is what Save is for.
*/
@@ -211,7 +211,7 @@ export const HomePanel = () => {
</section>
<section className="flex flex-col gap-3 rounded-lg border border-border p-4">
<h3 className="text-sm font-medium">Gallery</h3>
<h3 className="text-sm font-medium">Carousel</h3>
{/*
A picker, not a list.
@@ -233,19 +233,19 @@ export const HomePanel = () => {
className="w-full rounded-md border border-border bg-background px-3 py-2 text-xs"
>
<option value="">None</option>
{carousels.map((gallery) => (
<option key={gallery.id} value={gallery.id}>
{gallery.name} · {gallery.items.length} item{gallery.items.length === 1 ? '' : 's'}
{carousels.map((carousel) => (
<option key={carousel.id} value={carousel.id}>
{carousel.name} · {carousel.items.length} item{carousel.items.length === 1 ? '' : 's'}
</option>
))}
</select>
{/* Only while it is true: the old rows are what renders until a gallery is chosen, and saying so
{/* Only while it is true: the old rows are what renders until a carousel is chosen, and saying so
beats a carousel that appears to come from nowhere. */}
{home.carousel_id === null && home.gallery.length > 0 && (
{home.carousel_id === null && home.carousel.length > 0 && (
<p className="text-xs text-muted-foreground/70">
Showing {home.gallery.length} item{home.gallery.length === 1 ? '' : 's'} from the old
per-page list. Choosing a gallery replaces it.
Showing {home.carousel.length} item{home.carousel.length === 1 ? '' : 's'} from the old
per-page list. Choosing a carousel replaces it.
</p>
)}
</section>