a gallery is a carousel

It always rendered as one — data-carousel, a snap track, two arrows, driven by
carousel.client.js. The entity was the last thing still calling it something
else, and that collided with the site's own Gallery section: two nav rows
reading Gallery and Galleries, meaning a list of objects and a list of
photograph sets.

GalleriesPanel, GalleryPicker and GalleryItems become Carousels*, the endpoint
becomes /fofos-admin/carousels, the block kind becomes carousel and
subject-gallery becomes subject-carousel. The publish allowlist follows the
renamed tables.

What did NOT move: GALLERY_CONFIG at /fofos-admin/gallery, subjects.ts's
base '/gallery', and home_gallery_media. Those are the Gallery section and the
legacy rows, and both are correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 18:01:15 +01:00
co-authored by Claude Opus 5
parent 5e5c06e76e
commit 15042ccc76
19 changed files with 148 additions and 148 deletions
+29 -29
View File
@@ -19,7 +19,7 @@ import { paintMeasure, paintSelection, type CanvasComponent, type CanvasSection
const LABELS: Record<string, string> = {
'free-text': 'Text',
media: 'Media',
gallery: 'Gallery',
carousel: 'Carousel',
'contact-form': 'Contact form',
partners: 'Partners',
collections: 'Collections',
@@ -28,7 +28,7 @@ const LABELS: Record<string, string> = {
'subject-title': 'Name',
'subject-description': 'Description',
'subject-specs': 'Specs',
'subject-gallery': 'Photographs',
'subject-carousel': 'Photographs',
};
/*
@@ -47,14 +47,14 @@ const PLACES_ONLY: string[] = [
'subject-title',
'subject-description',
'subject-specs',
'subject-gallery',
'subject-carousel',
];
import { AddSectionDialog, type SectionLayout } from './AddSectionDialog';
import { FreeTextEditor } from './FreeTextEditor';
import { SpacingEditor, type Padding } from './SpacingEditor';
import { BandSettings } from './BandSettings';
import { BlockSettings } from './BlockSettings';
import { GalleryItems } from './GalleryItems';
import { CarouselItems } from './CarouselItems';
import { PartnersEditor } from './PartnersEditor';
import { BlockMedia } from './BlockMedia';
import { PageStructure } from './PageStructure';
@@ -89,7 +89,7 @@ const shapeOf = (block: { content?: { url?: string; ratio?: number } }): string
import { useEditorSlot } from '../useEditorSlot';
import { AddComponentDialog, type ComponentKind } from './AddComponentDialog';
import { MediaPicker } from '../MediaPicker';
import { GalleryPicker } from '../GalleryPicker';
import { CarouselPicker } from '../CarouselPicker';
import { pageFromParam, pageParam, usePages, useSections, type Page } from '../usePages';
/**
@@ -165,14 +165,14 @@ export const PageBuilderPanel = () => {
Which gallery a block shows, while it is being chosen.
The same shape as `picking` and for the same reason: choosing a set is one act with one outcome, so
there is nothing to draft and nothing to cancel out of afterwards. `galleryId` rides along so
there is nothing to draft and nothing to cancel out of afterwards. `carouselId` rides along so
reopening the picker can show where the block already stands.
*/
const [choosingGallery, setChoosingGallery] = useState<{
const [choosingCarousel, setChoosingCarousel] = useState<{
id: number | null;
sectionId: number;
column: number;
galleryId: number | null;
carouselId: number | null;
} | null>(null);
const [editing, setEditing] = useState<{ id: number | null; sectionId: number; column: number; html: string } | null>(
@@ -248,7 +248,7 @@ export const PageBuilderPanel = () => {
/*
Selecting a block shows its settings. It does not open a picker.
`editComponent` means "edit what is IN this block", and for media and galleries that is a modal
`editComponent` means "edit what is IN this block", and for media and carousels that is a modal
over the whole screen. Firing it on mere selection meant clicking a row in the nav threw a dialog
at you before you had said you wanted one — those kinds get a button in the settings instead.
@@ -463,7 +463,7 @@ export const PageBuilderPanel = () => {
// Each kind opens its own editor. A kind with no case here cannot be created, which is the point.
if (kind === 'free-text') setEditing({ id: null, ...at, html: '' });
if (kind === 'media') setPicking({ id: null, ...at });
if (kind === 'gallery') setChoosingGallery({ id: null, ...at, galleryId: null });
if (kind === 'carousel') setChoosingCarousel({ id: null, ...at, carouselId: null });
if (PLACES_ONLY.includes(kind)) void createEmpty(kind, at);
};
@@ -494,23 +494,23 @@ export const PageBuilderPanel = () => {
};
/*
A gallery block is a REFERENCE, saved as soon as the set is chosen — see GalleryPicker.
A gallery block is a REFERENCE, saved as soon as the set is chosen — see CarouselPicker.
Identical in shape to saveVideo because it is the same interaction: pick one thing, store one
value. The two are left as two functions rather than one generic saver, because what they store
happens to be one field each today and a shared helper would be an abstraction over a coincidence.
*/
const saveGallery = async (galleryId: number) => {
if (!page || !choosingGallery) return;
const base = `/fofos-admin/pages/${page.id}/sections/${choosingGallery.sectionId}/components`;
const target = choosingGallery;
setChoosingGallery(null);
const saveCarousel = async (carouselId: number) => {
if (!page || !choosingCarousel) return;
const base = `/fofos-admin/pages/${page.id}/sections/${choosingCarousel.sectionId}/components`;
const target = choosingCarousel;
setChoosingCarousel(null);
setError(null);
try {
if (target.id === null) {
await clientRef.current.post(base, { kind: 'gallery', column: target.column, content: { galleryId } });
await clientRef.current.post(base, { kind: 'carousel', column: target.column, content: { carouselId } });
} else {
await clientRef.current.patch(`${base}/${target.id}`, { content: { galleryId } });
await clientRef.current.patch(`${base}/${target.id}`, { content: { carouselId } });
}
setRendered((n) => n + 1);
} catch (failure) {
@@ -590,12 +590,12 @@ export const PageBuilderPanel = () => {
}
if (component.kind === 'gallery') {
const held = owner.components.find((one) => one.id === component.id)?.content?.galleryId;
setChoosingGallery({
const held = owner.components.find((one) => one.id === component.id)?.content?.carouselId;
setChoosingCarousel({
id: component.id,
sectionId: owner.id,
column: component.column,
galleryId: typeof held === 'number' ? held : null,
carouselId: typeof held === 'number' ? held : null,
});
return;
}
@@ -805,14 +805,14 @@ export const PageBuilderPanel = () => {
</Button>
{/*
The gallery's own contents, here rather than a link to the Galleries section.
The gallery's own contents, here rather than a link to the Carousels section.
A gallery block on a page IS a gallery, and being sent elsewhere to reorder it means
losing the page you were arranging it for. `onChanged` reloads the frame, because the
page draws this set and has just been given a different one.
*/}
{typeof block.content?.galleryId === 'number' && (
<GalleryItems galleryId={block.content.galleryId} onChanged={() => setRendered((n) => n + 1)} />
{typeof block.content?.carouselId === 'number' && (
<CarouselItems carouselId={block.content.carouselId} onChanged={() => setRendered((n) => n + 1)} />
)}
</div>
)}
@@ -1050,11 +1050,11 @@ export const PageBuilderPanel = () => {
title="Choose a photograph or a video"
/>
<GalleryPicker
open={choosingGallery !== null}
onClose={() => setChoosingGallery(null)}
onPick={(galleryId) => void saveGallery(galleryId)}
selected={choosingGallery?.galleryId ?? null}
<CarouselPicker
open={choosingCarousel !== null}
onClose={() => setChoosingCarousel(null)}
onPick={(carouselId) => void saveCarousel(carouselId)}
selected={choosingCarousel?.carouselId ?? null}
/>
</div>