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:
+3
-3
@@ -41,7 +41,7 @@ const CONTENT_TABLES = [
|
||||
/*
|
||||
The page builder's own tables, and the reason they are FIRST.
|
||||
|
||||
Parents before children: a section belongs to a page and a component to a section. `galleries` sits
|
||||
Parents before children: a section belongs to a page and a component to a section. `carousels` sits
|
||||
below them because a gallery block names a set by id — carried in JSON rather than as a foreign
|
||||
key, so nothing enforces the order, which is exactly why it is written down.
|
||||
|
||||
@@ -60,8 +60,8 @@ const CONTENT_TABLES = [
|
||||
'home_content',
|
||||
'home_gallery_media',
|
||||
'about_content',
|
||||
'galleries',
|
||||
'gallery_items',
|
||||
'carousels',
|
||||
'carousel_items',
|
||||
'collections',
|
||||
'collection_items',
|
||||
'collection_item_media',
|
||||
|
||||
@@ -5,17 +5,17 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
import { readableError } from './errors';
|
||||
|
||||
/**
|
||||
* Choose WHICH gallery a block shows.
|
||||
* Choose WHICH carousel a block shows.
|
||||
*
|
||||
* ── A reference, not a copy ──
|
||||
*
|
||||
* `galleries` is a named, ordered set built to be shown in more than one place — the old home carousel
|
||||
* already read one by id. So a gallery block stores that id and owns nothing else: reordering the set
|
||||
* `carousels` is a named, ordered set built to be shown in more than one place — the old home carousel
|
||||
* already read one by id. So a carousel block stores that id and owns nothing else: reordering the set
|
||||
* reorders every block pointing at it, and a photograph swapped there is swapped everywhere. That is
|
||||
* the whole reason the set has a name instead of being a list of files on a page.
|
||||
*
|
||||
* It follows that this picker chooses a SET, and editing what is IN the set happens where the set
|
||||
* lives — under Galleries. A block that could also rearrange its own pictures would be a second place
|
||||
* lives — under Carousels. A block that could also rearrange its own pictures would be a second place
|
||||
* to do the same job, and the two would disagree.
|
||||
*
|
||||
* ── No thumbnails, deliberately ──
|
||||
@@ -23,17 +23,17 @@ import { readableError } from './errors';
|
||||
* The plugin has no way to address a rendition: `MediaPicker` hands that job to the platform's file
|
||||
* browser, and every path stored here names an ORIGINAL that a browser cannot display. Rather than
|
||||
* build a second, worse image pipeline for a dialog, this shows the name and the count — which is what
|
||||
* tells two galleries apart, since the name is the thing somebody chose.
|
||||
* tells two carousels apart, since the name is the thing somebody chose.
|
||||
*/
|
||||
|
||||
export type GallerySummary = {
|
||||
export type CarouselSummary = {
|
||||
id: number;
|
||||
name: string;
|
||||
slug: string;
|
||||
items: unknown[];
|
||||
};
|
||||
|
||||
export const GalleryPicker = ({
|
||||
export const CarouselPicker = ({
|
||||
open,
|
||||
onClose,
|
||||
onPick,
|
||||
@@ -41,7 +41,7 @@ export const GalleryPicker = ({
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onPick: (galleryId: number) => void;
|
||||
onPick: (carouselId: number) => void;
|
||||
/** The one this block already shows, so reopening the picker says where it stands. */
|
||||
selected?: number | null;
|
||||
}) => {
|
||||
@@ -49,19 +49,19 @@ export const GalleryPicker = ({
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const [galleries, setGalleries] = useState<GallerySummary[] | null>(null);
|
||||
const [carousels, setCarousels] = useState<CarouselSummary[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Re-read on every open rather than once. A gallery created in the other panel while this session
|
||||
// Re-read on every open rather than once. A carousel created in the other panel while this session
|
||||
// has been running would otherwise be missing from a list that looks complete.
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
setGalleries(null);
|
||||
setCarousels(null);
|
||||
setError(null);
|
||||
void (async () => {
|
||||
try {
|
||||
const response = await clientRef.current.get<{ data: GallerySummary[] }>('/fofos-admin/galleries');
|
||||
setGalleries(response.data);
|
||||
const response = await clientRef.current.get<{ data: CarouselSummary[] }>('/fofos-admin/carousels');
|
||||
setCarousels(response.data);
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
}
|
||||
@@ -72,12 +72,12 @@ export const GalleryPicker = ({
|
||||
<Dialog open={open} onOpenChange={(next) => !next && onClose()}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Choose a gallery</DialogTitle>
|
||||
<DialogTitle>Choose a carousel</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
|
||||
{!galleries && !error && (
|
||||
{!carousels && !error && (
|
||||
<p className="flex items-center gap-2 p-4 text-xs text-muted-foreground">
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
Loading…
|
||||
@@ -85,29 +85,29 @@ export const GalleryPicker = ({
|
||||
)}
|
||||
|
||||
{/* Nothing to choose is a real state and says what to do about it, rather than an empty box.
|
||||
Creating one belongs under Galleries — see the note above about one place per job. */}
|
||||
{galleries?.length === 0 && (
|
||||
Creating one belongs under Carousels — see the note above about one place per job. */}
|
||||
{carousels?.length === 0 && (
|
||||
<p className="p-4 text-xs text-muted-foreground">
|
||||
No galleries yet. Make one under Galleries, then come back.
|
||||
No carousels yet. Make one under Carousels, then come back.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
{galleries?.map((gallery) => {
|
||||
const count = Array.isArray(gallery.items) ? gallery.items.length : 0;
|
||||
const current = gallery.id === selected;
|
||||
{carousels?.map((carousel) => {
|
||||
const count = Array.isArray(carousel.items) ? carousel.items.length : 0;
|
||||
const current = carousel.id === selected;
|
||||
return (
|
||||
<button
|
||||
key={gallery.id}
|
||||
key={carousel.id}
|
||||
type="button"
|
||||
onClick={() => onPick(gallery.id)}
|
||||
onClick={() => onPick(carousel.id)}
|
||||
className={`flex items-center gap-3 rounded-lg border p-3 text-left transition-colors ${
|
||||
current ? 'border-primary bg-primary/10' : 'border-border hover:border-primary hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<Images className="size-4 shrink-0 text-muted-foreground" />
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-sm">{gallery.name}</span>
|
||||
<span className="block truncate text-sm">{carousel.name}</span>
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
{count === 1 ? '1 item' : `${count} items`}
|
||||
</span>
|
||||
+2
-2
@@ -7,7 +7,7 @@ import { AdministrationPanel } from './AdministrationPanel';
|
||||
import { MediaPanel } from './MediaPanel';
|
||||
import { CollectionsPanel } from './sections/CollectionsPanel';
|
||||
import { PiecesPanel } from './sections/PiecesPanel';
|
||||
import { GalleriesPanel } from './sections/GalleriesPanel';
|
||||
import { CarouselsPanel } from './sections/CarouselsPanel';
|
||||
import { LayoutPanel } from './sections/LayoutPanel';
|
||||
import { PageBuilderPanel } from './sections/PageBuilderPanel';
|
||||
import { ViewModeProvider } from './ViewMode';
|
||||
@@ -50,7 +50,7 @@ const PANELS: Partial<Record<SectionKey, () => React.JSX.Element>> = {
|
||||
layout: LayoutPanel,
|
||||
collections: CollectionsPanel,
|
||||
pieces: PiecesPanel,
|
||||
galleries: GalleriesPanel,
|
||||
carousels: CarouselsPanel,
|
||||
messages: MessagesPanel,
|
||||
administration: AdministrationPanel,
|
||||
media: MediaPanel,
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ export const EditorPanel = () => {
|
||||
{/*
|
||||
Only where there is a page to look at.
|
||||
|
||||
The toggle sets the width the PREVIEW renders at, and Galleries, Media and Administration have
|
||||
The toggle sets the width the PREVIEW renders at, and Carousels, Media and Administration have
|
||||
no preview — the control would be three buttons that do nothing visible. Named here rather
|
||||
than read from `usePreviewable`, which is a context inside the detail panel and this is its
|
||||
sibling.
|
||||
|
||||
+2
-2
@@ -60,9 +60,9 @@ export type CanvasComponent = {
|
||||
/*
|
||||
One optional field per kind that the panel reads back. Not `Record<string, unknown>`: every reader
|
||||
would then need a cast, and the point of naming them is that the compiler knows `free-text` holds
|
||||
`html` and `gallery` holds `galleryId`.
|
||||
`html` and `gallery` holds `carouselId`.
|
||||
*/
|
||||
content?: { html?: string; galleryId?: number | null; url?: string; ratio?: number; crop?: string };
|
||||
content?: { html?: string; carouselId?: number | null; url?: string; ratio?: number; crop?: string };
|
||||
padding?: Record<string, { x?: number; y?: number }>;
|
||||
};
|
||||
export type CanvasSection = {
|
||||
|
||||
+5
-5
@@ -29,8 +29,8 @@ const relativeTo = (path: string): string | null => {
|
||||
|
||||
export const fileActions: FileAction[] = [
|
||||
{
|
||||
key: 'fofos-admin:create-gallery',
|
||||
label: 'Create Gallery',
|
||||
key: 'fofos-admin:create-carousel',
|
||||
label: 'Carousel from folder',
|
||||
icon: Images,
|
||||
/*
|
||||
A folder, or any number of files — both are ways of saying "these, in this order".
|
||||
@@ -76,10 +76,10 @@ export const fileActions: FileAction[] = [
|
||||
paths = body.files;
|
||||
}
|
||||
/*
|
||||
Handed over in the URL, and the Galleries panel does the asking.
|
||||
Handed over in the URL, and the Carousels panel does the asking.
|
||||
|
||||
The action has no dialog of its own on purpose: naming a gallery, seeing the slug it will get
|
||||
and watching the items land belongs on the screen that owns galleries, not in a modal over a
|
||||
and watching the items land belongs on the screen that owns carousels, not in a modal over a
|
||||
file browser that would then have to explain where the result went.
|
||||
*/
|
||||
// The section is a PATH now, so this names it rather than editing the current query. `create` and
|
||||
@@ -87,7 +87,7 @@ export const fileActions: FileAction[] = [
|
||||
const params = new URLSearchParams();
|
||||
params.set('create', entries.length === 1 && entries[0]!.type === 'directory' ? entries[0]!.name : '');
|
||||
params.set('from', paths.join('\n'));
|
||||
window.history.pushState({}, '', `${href('galleries')}?${params.toString()}`);
|
||||
window.history.pushState({}, '', `${href('carousels')}?${params.toString()}`);
|
||||
// The router listens for this; pushState alone does not notify it.
|
||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||
},
|
||||
|
||||
+2
-2
@@ -18,7 +18,7 @@ import { DEFAULT_SECTION, isSectionKey, type SectionKey } from './sections';
|
||||
* ── Why the pathname and not `useParams` ──
|
||||
*
|
||||
* Which parameters exist depends on which route matched: `/fofos-admin/layout/header` has a `band` and
|
||||
* `/fofos-admin/galleries` has nothing. A panel would then have to know its own route to read its own
|
||||
* `/fofos-admin/carousels` has nothing. A panel would then have to know its own route to read its own
|
||||
* position, and every panel is rendered on all of them.
|
||||
*
|
||||
* Two segments after the mount, always in the same places. One reader, every route.
|
||||
@@ -58,7 +58,7 @@ export const useNav = (): {
|
||||
* A link to a section, or to something inside one.
|
||||
*
|
||||
* The query string is dropped, and that is the point rather than an omission. `?device=tablet` is how
|
||||
* you are looking at the Layout preview; carrying it into Galleries would be state from one screen
|
||||
* you are looking at the Layout preview; carrying it into Carousels would be state from one screen
|
||||
* deciding something on another. A panel that wants a parameter to survive says so at its own link.
|
||||
*/
|
||||
export const href = (
|
||||
|
||||
+12
-12
@@ -1,5 +1,5 @@
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import { Boxes, Cog, Files, FolderOpen, Images, Mail, PanelsTopLeft, Shapes } from 'lucide-react';
|
||||
import { Boxes, Cog, Files, FolderOpen, GalleryHorizontal, Images, Mail, PanelsTopLeft, Shapes } from 'lucide-react';
|
||||
|
||||
/**
|
||||
* The sections of the fofos admin, in nav order.
|
||||
@@ -40,7 +40,7 @@ export type SectionKey =
|
||||
| 'administration'
|
||||
| 'collections'
|
||||
| 'pieces'
|
||||
| 'galleries'
|
||||
| 'carousels'
|
||||
| 'media'
|
||||
| 'messages'
|
||||
| 'layout'
|
||||
@@ -120,7 +120,7 @@ export const SECTIONS: Section[] = [
|
||||
},
|
||||
{
|
||||
/*
|
||||
Galleries — an entity of their own, not a list hanging off a page.
|
||||
Carousels — an entity of their own, not a list hanging off a page.
|
||||
|
||||
Above Media and pinned with it, because the two are the same kind of thing: the raw material the
|
||||
site is built from, held on this machine. Media is the files; a gallery is a set of them somebody
|
||||
@@ -140,7 +140,7 @@ export const SECTIONS: Section[] = [
|
||||
path: '/fofos-admin/collections',
|
||||
built: true,
|
||||
/*
|
||||
Same reason as Galleries: a collection is a ROW read through the site's admin API, so this is
|
||||
Same reason as Carousels: a collection is a ROW read through the site's admin API, so this is
|
||||
useless without the development server answering. What is different is that the row is only half
|
||||
of it — which collections EXIST is a question about `originals/COLLECTIONS/`, and the folders are
|
||||
registered as this list is read.
|
||||
@@ -153,9 +153,9 @@ export const SECTIONS: Section[] = [
|
||||
The Gallery's pieces.
|
||||
|
||||
Called Pieces, not Gallery. The section on the site is Gallery and its items are peças, and there
|
||||
is already a `galleries` section here meaning something else entirely — a named set of media. Two
|
||||
nav rows called Gallery and Galleries, meaning a list of objects and a list of photograph sets,
|
||||
is a question somebody would have to answer every time they looked at it.
|
||||
is a Carousels section here meaning something else entirely — a named, ordered set of media. The
|
||||
two were called Gallery and Galleries once, meaning a list of objects and a list of photograph
|
||||
sets, and that is what the rename to Carousels was for.
|
||||
|
||||
Below Collections because it is the same object one level flatter, and it shares its panel — see
|
||||
sections/subjects.ts.
|
||||
@@ -172,12 +172,12 @@ export const SECTIONS: Section[] = [
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
key: 'galleries',
|
||||
label: 'Galleries',
|
||||
icon: Images,
|
||||
key: 'carousels',
|
||||
label: 'Carousels',
|
||||
icon: GalleryHorizontal,
|
||||
hint: 'Named sets of media, reusable across the site',
|
||||
// `/fofos-admin/galleries` — it reads the site's own admin API, unlike Media beside it.
|
||||
path: '/fofos-admin/galleries',
|
||||
// `/fofos-admin/carousels` — it reads the site's own admin API, unlike Media beside it.
|
||||
path: '/fofos-admin/carousels',
|
||||
built: true,
|
||||
requires: 'connection',
|
||||
pinned: true,
|
||||
|
||||
@@ -26,7 +26,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/u
|
||||
export type ComponentKind =
|
||||
| 'free-text'
|
||||
| 'media'
|
||||
| 'gallery'
|
||||
| 'carousel'
|
||||
| 'contact-form'
|
||||
| 'partners'
|
||||
| 'collections'
|
||||
@@ -35,7 +35,7 @@ export type ComponentKind =
|
||||
| 'subject-title'
|
||||
| 'subject-description'
|
||||
| 'subject-specs'
|
||||
| 'subject-gallery';
|
||||
| 'subject-carousel';
|
||||
|
||||
/*
|
||||
The blocks that draw whatever the page is ABOUT.
|
||||
@@ -48,7 +48,7 @@ export const SUBJECT_KINDS: ComponentKind[] = [
|
||||
'subject-title',
|
||||
'subject-description',
|
||||
'subject-specs',
|
||||
'subject-gallery',
|
||||
'subject-carousel',
|
||||
];
|
||||
|
||||
type Choice = {
|
||||
@@ -62,7 +62,7 @@ type Choice = {
|
||||
* `kind: null` means not built.
|
||||
*
|
||||
* Named rather than omitted, and the ones named are things the site already HAS — media, video and
|
||||
* galleries all exist as concepts here. That is the difference between saying what is coming and
|
||||
* carousels all exist as concepts here. That is the difference between saying what is coming and
|
||||
* inventing a roadmap. Gallery stopped being one of them on 2026-08-31: the set, its ordering and the
|
||||
* carousel that draws it were all already built, so the block is a reference to one.
|
||||
*/
|
||||
@@ -77,7 +77,7 @@ export const CHOICES: Choice[] = [
|
||||
always done.
|
||||
*/
|
||||
{ kind: 'media', label: 'Media', hint: 'A photograph or a video, from the media store', Icon: Image },
|
||||
{ kind: 'gallery', label: 'Gallery', hint: 'A named set of media', Icon: Images },
|
||||
{ kind: 'carousel', label: 'Carousel', hint: 'A named set of media', Icon: Images },
|
||||
/*
|
||||
Three that place something the site already draws.
|
||||
|
||||
@@ -108,7 +108,7 @@ export const CHOICES: Choice[] = [
|
||||
{ kind: 'subject-title', label: 'Its name', hint: 'What this page is about', Icon: Heading },
|
||||
{ kind: 'subject-description', label: 'Its description', hint: 'What was written about it', Icon: Type },
|
||||
{ kind: 'subject-specs', label: 'Its specs', hint: 'Measurement, material, colours, and the rest', Icon: Ruler },
|
||||
{ kind: 'subject-gallery', label: 'Its photographs', hint: 'The gallery it points at', Icon: Images },
|
||||
{ kind: 'subject-carousel', label: 'Its photographs', hint: 'The gallery it points at', Icon: Images },
|
||||
];
|
||||
|
||||
export const AddComponentDialog = ({
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { CanvasComponent, CanvasSection } from '../canvas';
|
||||
*
|
||||
* The page's structure nests this under every band, and the band's own editor shows just its own.
|
||||
* They are the same list of the same things with the same actions, so they are the same component —
|
||||
* the galleries panel taught that lesson once already.
|
||||
* the carousels panel taught that lesson once already.
|
||||
*
|
||||
* ── Columns, because adding needs one ──
|
||||
*
|
||||
|
||||
@@ -10,7 +10,7 @@ import { thumbUrl } from '../thumbnail';
|
||||
/**
|
||||
* One gallery's contents: what is in it, in what order, and what each picture is called.
|
||||
*
|
||||
* ── Why it is here and not a link to Galleries ──
|
||||
* ── Why it is here and not a link to Carousels ──
|
||||
*
|
||||
* A gallery block on a page IS a gallery, and being sent to another section to reorder it means
|
||||
* losing the page you were arranging it for. The set is a thing of its own — that is why it has its
|
||||
@@ -18,36 +18,36 @@ import { thumbUrl } from '../thumbnail';
|
||||
*
|
||||
* ── It reads the gallery itself ──
|
||||
*
|
||||
* `GET /galleries/:id`, rather than being handed one. The block knows an id and nothing else, and a
|
||||
* `GET /carousels/:id`, rather than being handed one. The block knows an id and nothing else, and a
|
||||
* component that fetches what it needs can be dropped anywhere something knows an id.
|
||||
*
|
||||
* ── Duplication, stated plainly ──
|
||||
*
|
||||
* `GalleriesPanel` still draws its own copy of this list. That is a real duplicate and it should adopt
|
||||
* `CarouselsPanel` still draws its own copy of this list. That is a real duplicate and it should adopt
|
||||
* this component — its version carries the same operations against the same routes, plus creating and
|
||||
* deleting whole galleries, which this deliberately does not.
|
||||
* deleting whole carousels, which this deliberately does not.
|
||||
*/
|
||||
|
||||
type Item = { id: number; url: string; alt: string | null };
|
||||
type Gallery = { id: number; name: string; slug: string; items: Item[] };
|
||||
|
||||
export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onChanged?: () => void }) => {
|
||||
export const CarouselItems = ({ carouselId, onChanged }: { carouselId: number; onChanged?: () => void }) => {
|
||||
const client = useClient();
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const [gallery, setGallery] = useState<Gallery | null>(null);
|
||||
const [gallery, setCarousel] = useState<Gallery | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [picking, setPicking] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
setGallery((await clientRef.current.get<{ data: Gallery }>(`/fofos-admin/galleries/${galleryId}`)).data);
|
||||
setCarousel((await clientRef.current.get<{ data: Gallery }>(`/fofos-admin/carousels/${carouselId}`)).data);
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
}
|
||||
}, [galleryId]);
|
||||
}, [carouselId]);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
@@ -95,7 +95,7 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
||||
|
||||
next.splice(to, 0, next.splice(from, 1)[0]!);
|
||||
orderedRef.current = next;
|
||||
setGallery({ ...gallery, items: next });
|
||||
setCarousel({ ...gallery, items: next });
|
||||
};
|
||||
|
||||
const commitOrder = () => {
|
||||
@@ -106,7 +106,7 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
||||
if (!ordered) return;
|
||||
|
||||
void send(() =>
|
||||
clientRef.current.put(`/fofos-admin/galleries/${galleryId}/items/order`, { ids: ordered.map((one) => one.id) }),
|
||||
clientRef.current.put(`/fofos-admin/carousels/${carouselId}/items/order`, { ids: ordered.map((one) => one.id) }),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
||||
const value = event.target.value.trim();
|
||||
if (value === (item.alt ?? '')) return;
|
||||
void send(() =>
|
||||
clientRef.current.patch(`/fofos-admin/galleries/${galleryId}/items/${item.id}`, {
|
||||
clientRef.current.patch(`/fofos-admin/carousels/${carouselId}/items/${item.id}`, {
|
||||
alt: value || null,
|
||||
}),
|
||||
);
|
||||
@@ -190,7 +190,7 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
||||
className="h-7 px-1.5 text-destructive"
|
||||
disabled={busy}
|
||||
onClick={() =>
|
||||
void send(() => clientRef.current.delete(`/fofos-admin/galleries/${galleryId}/items/${item.id}`))
|
||||
void send(() => clientRef.current.delete(`/fofos-admin/carousels/${carouselId}/items/${item.id}`))
|
||||
}
|
||||
>
|
||||
<Trash2 className="size-3.5" />
|
||||
@@ -211,7 +211,7 @@ export const GalleryItems = ({ galleryId, onChanged }: { galleryId: number; onCh
|
||||
onClose={() => setPicking(false)}
|
||||
onPick={(url) => {
|
||||
setPicking(false);
|
||||
void send(() => clientRef.current.post(`/fofos-admin/galleries/${galleryId}/items`, { urls: [url] }));
|
||||
void send(() => clientRef.current.post(`/fofos-admin/carousels/${carouselId}/items`, { urls: [url] }));
|
||||
}}
|
||||
title="Add to this gallery"
|
||||
/>
|
||||
@@ -6,10 +6,10 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { readableError } from '../errors';
|
||||
import { useEditorSlot } from '../useEditorSlot';
|
||||
import { GalleryItems } from './GalleryItems';
|
||||
import { CarouselItems } from './CarouselItems';
|
||||
|
||||
/**
|
||||
* Galleries — named, ordered sets of media, reusable anywhere on the site.
|
||||
* Carousels — named, ordered sets of media, reusable anywhere on the site.
|
||||
*
|
||||
* ── Why a gallery is not a list on a page ──
|
||||
*
|
||||
@@ -28,12 +28,12 @@ import { GalleryItems } from './GalleryItems';
|
||||
type Item = { id: number; url: string; alt: string | null; sort_order: number };
|
||||
type Gallery = { id: number; name: string; slug: string; items: Item[] };
|
||||
|
||||
export const GalleriesPanel = () => {
|
||||
export const CarouselsPanel = () => {
|
||||
const client = useClient();
|
||||
const clientRef = useRef(client);
|
||||
clientRef.current = client;
|
||||
|
||||
const [galleries, setGalleries] = useState<Gallery[] | null>(null);
|
||||
const [carousels, setCarousels] = useState<Gallery[] | null>(null);
|
||||
const [openId, setOpenId] = useState<number | null>(null);
|
||||
const [name, setName] = useState('');
|
||||
/** Paths handed over by the context menu, waiting for a name. */
|
||||
@@ -46,11 +46,11 @@ export const GalleriesPanel = () => {
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const response = await clientRef.current.get<{ data: Gallery[] }>('/fofos-admin/galleries');
|
||||
setGalleries(response.data);
|
||||
const response = await clientRef.current.get<{ data: Gallery[] }>('/fofos-admin/carousels');
|
||||
setCarousels(response.data);
|
||||
} catch (failure) {
|
||||
setError(readableError(failure));
|
||||
setGalleries([]);
|
||||
setCarousels([]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -73,7 +73,7 @@ export const GalleriesPanel = () => {
|
||||
|
||||
useEffect(() => {
|
||||
setEditorRef.current(
|
||||
openId === null ? null : <GalleryItems galleryId={openId} onChanged={() => void load()} />,
|
||||
openId === null ? null : <CarouselItems carouselId={openId} onChanged={() => void load()} />,
|
||||
);
|
||||
}, [openId, load]);
|
||||
|
||||
@@ -85,7 +85,7 @@ export const GalleriesPanel = () => {
|
||||
|
||||
The action puts the chosen paths in the URL and sends you here rather than opening its own dialog:
|
||||
naming a gallery, seeing the slug it gets and watching the items land belongs on the screen that
|
||||
owns galleries. Consumed once and removed from the URL — a `?from=` left behind would recreate the
|
||||
owns carousels. Consumed once and removed from the URL — a `?from=` left behind would recreate the
|
||||
gallery on every refresh, which somebody discovers as four copies.
|
||||
*/
|
||||
const arriving = params.get('from');
|
||||
@@ -124,8 +124,8 @@ export const GalleriesPanel = () => {
|
||||
/*
|
||||
Renaming changes the NAME and leaves the slug exactly where it is.
|
||||
|
||||
Everything that points at a gallery points at its id — `collections.gallery_id`, and a gallery
|
||||
block's `content.galleryId`. Nothing renders through the slug, so a rename cannot break a reference
|
||||
Everything that points at a gallery points at its id — `collections.carousel_id`, and a gallery
|
||||
block's `content.carouselId`. Nothing renders through the slug, so a rename cannot break a reference
|
||||
today. It is left alone anyway, because a slug is an ADDRESS: it can end up in a link somebody
|
||||
sent, and the day something does resolve by it, a label having quietly moved it would be a very
|
||||
hard bug to find.
|
||||
@@ -136,7 +136,7 @@ export const GalleriesPanel = () => {
|
||||
const rename = () =>
|
||||
send('rename', async () => {
|
||||
if (!renaming?.name.trim()) return;
|
||||
await clientRef.current.patch(`/fofos-admin/galleries/${renaming.id}`, { name: renaming.name.trim() });
|
||||
await clientRef.current.patch(`/fofos-admin/carousels/${renaming.id}`, { name: renaming.name.trim() });
|
||||
setRenaming(null);
|
||||
});
|
||||
|
||||
@@ -144,17 +144,17 @@ export const GalleriesPanel = () => {
|
||||
send('create', async () => {
|
||||
// `urls` in the same request, so creating from a selection is one action — splitting it would
|
||||
// leave an empty gallery behind whenever the second call failed.
|
||||
await clientRef.current.post('/fofos-admin/galleries', { name: name.trim(), urls: pending });
|
||||
await clientRef.current.post('/fofos-admin/carousels', { name: name.trim(), urls: pending });
|
||||
setName('');
|
||||
setPending([]);
|
||||
});
|
||||
|
||||
if (!galleries) return <div className="p-5 text-xs text-muted-foreground">{error ?? 'Loading…'}</div>;
|
||||
if (!carousels) return <div className="p-5 text-xs text-muted-foreground">{error ?? 'Loading…'}</div>;
|
||||
|
||||
return (
|
||||
<div className="flex size-full flex-col gap-4 overflow-auto p-5">
|
||||
<section className="flex flex-col gap-3 rounded-lg border border-border p-4">
|
||||
<h3 className="text-sm font-medium">Galleries</h3>
|
||||
<h3 className="text-sm font-medium">Carousels</h3>
|
||||
|
||||
{error && <p className="text-xs text-destructive">{error}</p>}
|
||||
|
||||
@@ -193,13 +193,13 @@ export const GalleriesPanel = () => {
|
||||
The list, and only the list.
|
||||
|
||||
The contents of a gallery — its pictures, their order, their captions — are announced to the
|
||||
editor column and drawn by `GalleryItems`, which is the same component a gallery BLOCK on a
|
||||
editor column and drawn by `CarouselItems`, which is the same component a gallery BLOCK on a
|
||||
page uses. Two copies of that list existed for a day and this is the one that went.
|
||||
|
||||
So this panel is what a master list should be: which galleries there are, making one, deleting
|
||||
So this panel is what a master list should be: which carousels there are, making one, deleting
|
||||
one. What is inside the selected one is a different question, answered beside it.
|
||||
*/}
|
||||
{galleries.map((gallery) => {
|
||||
{carousels.map((gallery) => {
|
||||
const open = openId === gallery.id;
|
||||
return (
|
||||
<section
|
||||
@@ -283,7 +283,7 @@ export const GalleriesPanel = () => {
|
||||
className="shrink-0"
|
||||
onClick={() =>
|
||||
void send(`gallery:${gallery.id}`, () =>
|
||||
clientRef.current.delete(`/fofos-admin/galleries/${gallery.id}`),
|
||||
clientRef.current.delete(`/fofos-admin/carousels/${gallery.id}`),
|
||||
)
|
||||
}
|
||||
disabled={busy !== null}
|
||||
+12
-12
@@ -48,7 +48,7 @@ type Home = {
|
||||
/** 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;
|
||||
carousel_id: number | null;
|
||||
};
|
||||
|
||||
type Gallery = { id: number; name: string; slug: string; items: unknown[] };
|
||||
@@ -65,7 +65,7 @@ export const HomePanel = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saved, flashSaved] = useFlash();
|
||||
const [picking, setPicking] = useState(false);
|
||||
const [galleries, setGalleries] = useState<Gallery[]>([]);
|
||||
const [carousels, setCarousels] = useState<Gallery[]>([]);
|
||||
|
||||
/** Every write answers with the whole payload, so this is the only place state is set. */
|
||||
const apply = useCallback((data: Home) => {
|
||||
@@ -87,13 +87,13 @@ export const HomePanel = () => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
// The galleries to choose between, read from the section that owns them rather than embedded in the
|
||||
// The carousels 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([]));
|
||||
.get<{ data: Gallery[] }>('/fofos-admin/carousels')
|
||||
.then((response) => setCarousels(response.data))
|
||||
.catch(() => setCarousels([]));
|
||||
}, []);
|
||||
|
||||
const send = async (label: string, call: () => Promise<{ data: Home }>) => {
|
||||
@@ -217,15 +217,15 @@ export const HomePanel = () => {
|
||||
A picker, not a list.
|
||||
|
||||
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
|
||||
would drift: a caption fixed in one place and not the other. The page chooses; the Carousels
|
||||
section arranges.
|
||||
*/}
|
||||
<select
|
||||
value={home.gallery_id ?? ''}
|
||||
value={home.carousel_id ?? ''}
|
||||
onChange={(event) =>
|
||||
void send('gallery', () =>
|
||||
void send('carousel', () =>
|
||||
clientRef.current.patch<{ data: Home }>('/fofos-admin/home', {
|
||||
gallery_id: event.target.value ? Number(event.target.value) : null,
|
||||
carousel_id: event.target.value ? Number(event.target.value) : null,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -233,7 +233,7 @@ export const HomePanel = () => {
|
||||
className="w-full rounded-md border border-border bg-background px-3 py-2 text-xs"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{galleries.map((gallery) => (
|
||||
{carousels.map((gallery) => (
|
||||
<option key={gallery.id} value={gallery.id}>
|
||||
{gallery.name} · {gallery.items.length} item{gallery.items.length === 1 ? '' : 's'}
|
||||
</option>
|
||||
@@ -242,7 +242,7 @@ export const HomePanel = () => {
|
||||
|
||||
{/* 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 && (
|
||||
{home.carousel_id === null && home.gallery.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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,7 +19,7 @@ import { thumbUrl } from '../thumbnail';
|
||||
* That is worth saying out loud in the panel, because the editor sits inside a block on a page and
|
||||
* everything else in that column belongs to the page.
|
||||
*
|
||||
* ── The same shape as GalleryItems ──
|
||||
* ── The same shape as CarouselItems ──
|
||||
*
|
||||
* Drag to reorder, a grip on the left, one write at the end carrying every id. Deliberately, so the
|
||||
* two ordered lists in this admin are the same gesture rather than two things to learn.
|
||||
@@ -64,7 +64,7 @@ export const PartnersEditor = ({ onChanged }: { onChanged?: () => void }) => {
|
||||
}
|
||||
};
|
||||
|
||||
/* Reordering, live. Same as GalleryItems — see the note there. */
|
||||
/* Reordering, live. Same as CarouselItems — see the note there. */
|
||||
const [dragged, setDragged] = useState<number | null>(null);
|
||||
const orderedRef = useRef<Partner[] | null>(null);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ type RecordRow = { id: number; title: string; sort_order: number } & Record<stri
|
||||
* A thumbnail URL, authenticated by `?token=`.
|
||||
*
|
||||
* An `<img>` cannot send an Authorization header, which is why `user-middleware` and the permission
|
||||
* gate both accept the token in the query string. The same thing GalleriesPanel and the file browser
|
||||
* gate both accept the token in the query string. The same thing CarouselsPanel and the file browser
|
||||
* do — built without it first, and every picture 401s while the layout looks perfectly correct.
|
||||
*/
|
||||
const thumbUrl = (path: string): string => {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Images, Loader2, Plus, Trash2 } from 'lucide-react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { GalleryPicker } from '../GalleryPicker';
|
||||
import { CarouselPicker } from '../CarouselPicker';
|
||||
import { readableError } from '../errors';
|
||||
import { useEditorSlot } from '../useEditorSlot';
|
||||
import { thumbUrl } from '../thumbnail';
|
||||
@@ -27,7 +27,7 @@ import type { SubjectPhoto, SubjectRecord, SubjectSection } from './subjects';
|
||||
*
|
||||
* The photographs belong to the gallery, and so does their order — including which one leads, which
|
||||
* is simply the first. Making them look pressable here would promise a control that lives in
|
||||
* Galleries.
|
||||
* Carousels.
|
||||
*/
|
||||
export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: SubjectSection<T> }) => {
|
||||
const client = useClient();
|
||||
@@ -167,11 +167,11 @@ export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: Su
|
||||
);
|
||||
});
|
||||
|
||||
const chooseGallery = (galleryId: number) =>
|
||||
const chooseCarousel = (carouselId: number) =>
|
||||
send(async () => {
|
||||
if (!selected) return;
|
||||
setPicking(false);
|
||||
await clientRef.current.patch(`${section.endpoint}/${selected.id}`, { gallery_id: galleryId });
|
||||
await clientRef.current.patch(`${section.endpoint}/${selected.id}`, { carousel_id: carouselId });
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -230,11 +230,11 @@ export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: Su
|
||||
<div className="mb-4 flex flex-wrap items-center gap-2 border-b border-border pb-3">
|
||||
<Button size="sm" variant="outline" className="h-8 gap-1.5 text-xs" onClick={() => setPicking(true)}>
|
||||
<Images className="size-3.5" />
|
||||
{selected.gallery_id ? 'Change gallery' : 'Choose a gallery'}
|
||||
{selected.carousel_id ? 'Change gallery' : 'Choose a gallery'}
|
||||
</Button>
|
||||
<span className="min-w-0 flex-1" />
|
||||
{/* Deleting removes the ROW, never the photographs — they belong to a gallery, which
|
||||
belongs to Galleries. Said on the button so nobody has to find out. */}
|
||||
belongs to Carousels. Said on the button so nobody has to find out. */}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -251,7 +251,7 @@ export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: Su
|
||||
|
||||
{!selected ? (
|
||||
<p className="text-xs text-muted-foreground">Choose a {section.noun}, or make one.</p>
|
||||
) : !selected.gallery_id ? (
|
||||
) : !selected.carousel_id ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
No gallery yet. Choose one and its photographs appear here.
|
||||
</p>
|
||||
@@ -261,13 +261,13 @@ export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: Su
|
||||
</p>
|
||||
) : photos.length === 0 ? (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
That gallery is empty. Add photographs to it in Galleries.
|
||||
That gallery is empty. Add photographs to it in Carousels.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="mb-3 text-xs text-muted-foreground">
|
||||
{photos.length} photographs, in the gallery’s order — the first one leads.
|
||||
Reorder them in Galleries.
|
||||
Reorder them in Carousels.
|
||||
</p>
|
||||
<ul className="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3">
|
||||
{photos.map((photo) => (
|
||||
@@ -286,11 +286,11 @@ export const SubjectPanel = <T extends SubjectRecord>({ section }: { section: Su
|
||||
{/* `selected` guarded: the picker writes to whatever is open, and there is nothing to write to
|
||||
when nothing is. */}
|
||||
{selected && (
|
||||
<GalleryPicker
|
||||
<CarouselPicker
|
||||
open={picking}
|
||||
onClose={() => setPicking(false)}
|
||||
onPick={(galleryId) => void chooseGallery(galleryId)}
|
||||
selected={selected.gallery_id}
|
||||
onPick={(carouselId) => void chooseCarousel(carouselId)}
|
||||
selected={selected.carousel_id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ import { Boxes, Shapes } from 'lucide-react';
|
||||
export type SubjectRecord = {
|
||||
id: number;
|
||||
/** The gallery holding its media, or null while nobody has chosen one. */
|
||||
gallery_id: number | null;
|
||||
carousel_id: number | null;
|
||||
title: string;
|
||||
slug: string | null;
|
||||
description: string | null;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import { getHeaders } from 'hooks/useClient';
|
||||
*
|
||||
* ── Why this is a module ──
|
||||
*
|
||||
* It was copied into GalleriesPanel and again into RecordsPanel, and a third copy was about to go into
|
||||
* It was copied into CarouselsPanel and again into RecordsPanel, and a third copy was about to go into
|
||||
* MediaField. Three copies of a URL shape is three places to be wrong about the rung when the ladder
|
||||
* changes — which it did, four times in a day.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user