web: a photo can sit in every landing section at once

The picker was one dropdown, so a photo lived in exactly one place. The three
destinations are now independent checkboxes on the card, and the column holds
the set as a comma list — the landing page draws a photo in every section it
was ticked into, each still picking one of its own at random per visit.

Ticking nothing is what `off` used to be: the row is kept and the landing page
stops drawing it, which is what the old "not on the landing page" option did.
This commit is contained in:
2026-09-18 16:47:12 +07:00
parent ee2c402438
commit 260517c547
9 changed files with 148 additions and 83 deletions
+10 -7
View File
@@ -31,10 +31,10 @@ export interface SavedRecipe {
}
// Where a curated photo may appear on the landing page. `strip` is the
// community reel; the rest are the three live slots, each of which shows one
// random photo out of its set per page load. `off` keeps the row but takes it
// off the landing entirely — the curator's removal.
export type PhotoSlot = 'strip' | 'tester' | 'creator' | 'qr' | 'off';
// community reel; the rest are the three live sections, each of which shows one
// random photo out of its set per page load. A photo may sit in several at
// once; an empty set is the curator's removal from the landing.
export type PhotoSlot = 'strip' | 'tester' | 'creator' | 'qr';
// A strip contribution as the public sees it — the API never puts an email on
// this shape. `tag`/`title`/`meta` are the frame's own labels (the amber
@@ -43,7 +43,7 @@ export type PhotoSlot = 'strip' | 'tester' | 'creator' | 'qr' | 'off';
export interface Photo {
id: number;
createdAt: string;
slot: PhotoSlot;
slots: PhotoSlot[];
tag: string | null;
title: string | null;
meta: string | null;
@@ -223,8 +223,11 @@ export const api = {
adminListPhotos: () => call<{ photos: AdminPhoto[] }>('/admin/photos'),
adminDeletePhoto: (id: number) => call<void>(`/admin/photos/${id}`, { method: 'DELETE' }),
adminClearPhotos: () => call<{ removed: number }>('/admin/photos', { method: 'DELETE' }),
adminSetPhotoSlot: (id: number, slot: PhotoSlot) =>
call<{ id: number; slot: PhotoSlot }>(`/admin/photos/${id}`, { method: 'PATCH', body: JSON.stringify({ slot }) }),
adminSetPhotoSlots: (id: number, slots: PhotoSlot[]) =>
call<{ id: number; slots: PhotoSlot[] }>(`/admin/photos/${id}`, {
method: 'PATCH',
body: JSON.stringify({ slots }),
}),
// Moderation of an account. Both flags are reversible; `adminDeleteUser` is
// the final act and takes the account's photos and recipes with it.