the first photograph leads, and nothing says so twice

A collection named its own main picture. A gallery is already ORDERED — somebody drags it into the
sequence they want — so "which one leads" was being answered twice, in two screens, by two mechanisms
that could disagree. Reordering a gallery and then wondering why the card had not changed was a bug
waiting to be filed.

So the grid here decides nothing at all. It is what the collection SHOWS, in the gallery's order, and
the pictures are no longer buttons: making them look pressable would promise a control that lives in
another screen. Adding, reordering and framing all belong to Galleries, and now so does this.

The `cover` column is gone from both databases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-04 14:34:27 +01:00
co-authored by Claude Opus 5
parent bc494fb57a
commit ad988e4557
2 changed files with 13 additions and 39 deletions
+9 -33
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { useSearchParams } from 'react-router'; import { useSearchParams } from 'react-router';
import { Boxes, Check, Images, Loader2, Plus, Trash2 } from 'lucide-react'; import { Boxes, Images, Loader2, Plus, Trash2 } from 'lucide-react';
import { useClient } from 'hooks/useClient'; import { useClient } from 'hooks/useClient';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
@@ -176,14 +176,6 @@ export const CollectionsPanel = () => {
await clientRef.current.patch(`/fofos-admin/collections/${selected.id}`, { gallery_id: galleryId }); await clientRef.current.patch(`/fofos-admin/collections/${selected.id}`, { gallery_id: galleryId });
}); });
const chooseCover = (path: string) =>
send(async () => {
if (!selected) return;
// Pressing the one already chosen clears it, which is how "no choice, use the first" is reachable
// again without a second control that only does that.
await clientRef.current.patch(`/fofos-admin/collections/${selected.id}`, {
cover: selected.cover === path ? null : path,
});
}); });
@@ -279,36 +271,20 @@ export const CollectionsPanel = () => {
) : ( ) : (
<> <>
<p className="mb-3 text-xs text-muted-foreground"> <p className="mb-3 text-xs text-muted-foreground">
{photos.length} photographs, in the gallery&rsquo;s order. Click one to make it the main photo. {photos.length} photographs, in the gallery&rsquo;s order &mdash; the first one leads. Reorder
them in Galleries.
</p> </p>
{/* Not buttons. Nothing here is a decision — the order is the gallery's and so is which one
leads. Making them look pressable would promise a control that lives elsewhere. */}
<ul className="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3"> <ul className="grid grid-cols-[repeat(auto-fill,minmax(9rem,1fr))] gap-3">
{photos.map((photo) => { {photos.map((photo) => (
const isCover = selected.cover ? photo.path === selected.cover : photo === photos[0];
return (
<li key={photo.path}> <li key={photo.path}>
<button <div className="overflow-hidden rounded-md border border-border">
type="button"
disabled={busy}
onClick={() => void chooseCover(photo.path)}
className={`relative block w-full overflow-hidden rounded-md border transition-colors ${
isCover ? 'border-primary ring-1 ring-primary' : 'border-border hover:border-primary/50'
}`}
>
<img src={thumbUrl(photo.path, 320)} alt="" className="aspect-square w-full object-cover" /> <img src={thumbUrl(photo.path, 320)} alt="" className="aspect-square w-full object-cover" />
{isCover && ( </div>
<span className="absolute top-1.5 right-1.5 flex items-center gap-1 rounded bg-primary px-1.5 py-0.5 text-[10px] text-primary-foreground shadow">
<Check className="size-2.5" />
{/* "Main" rather than "Cover": the first one is the main photo by default, so
the badge has to be true of a picture nobody chose as well as one somebody
did. */}
Main
</span>
)}
</button>
<p className="mt-1 truncate font-mono text-[10px] text-muted-foreground/70">{photo.name}</p> <p className="mt-1 truncate font-mono text-[10px] text-muted-foreground/70">{photo.name}</p>
</li> </li>
); ))}
})}
</ul> </ul>
</> </>
)} )}
-2
View File
@@ -5,8 +5,6 @@ export type Collection = {
gallery_id: number | null; gallery_id: number | null;
title: string; title: string;
slug: string | null; slug: string | null;
/** A media PATH, the same string a gallery item holds — see the site's schema. */
cover: string | null;
description: string | null; description: string | null;
descriptive_text: string | null; descriptive_text: string | null;
measurement: string | null; measurement: string | null;