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
+29 -15
View File
@@ -284,7 +284,10 @@ try {
const rows = adminList.body?.photos ?? [];
check('an admin lists contributions', adminList.status === 200 && rows.length > 0);
check('the admin listing carries the owner', rows.some((r) => /@test\.local$/.test(r.email ?? '')));
check('a fresh upload lands in the strip slot', rows.find((r) => r.id === id)?.slot === 'strip');
check(
'a fresh upload lands in the strip section',
JSON.stringify(rows.find((r) => r.id === id)?.slots) === JSON.stringify(['strip']),
);
check('a plain member cannot delete', (await user.req(`/admin/photos/${id}`, { method: 'DELETE' })).status === 403);
// ---- placement ----------------------------------------------------------
@@ -295,27 +298,38 @@ try {
(await user.req(`/admin/photos/${id}`, {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ slot: 'qr' }),
body: JSON.stringify({ slots: ['qr'] }),
})).status === 403,
);
const placed = await patch(`/admin/photos/${id}`, { slot: 'qr' });
check('an admin moves a photo to a live slot', placed.status === 200 && placed.body?.slot === 'qr', JSON.stringify(placed.body));
const placed = await patch(`/admin/photos/${id}`, { slots: ['tester', 'creator'] });
check(
'the slot is public, the owner is not',
((await actor().req('/photos')).body?.photos ?? []).find((r) => r.id === id)?.slot === 'qr',
'an admin puts a photo in several sections at once',
placed.status === 200 && JSON.stringify(placed.body?.slots) === JSON.stringify(['tester', 'creator']),
JSON.stringify(placed.body),
);
// `off` is the curator's removal: the reel draws slot === 'strip' and the
// live slots draw their own, so the row shows up nowhere — but the uploader
// keeps it in their folder.
const off = await patch(`/admin/photos/${id}`, { slot: 'off' });
check('an admin takes a photo off the landing', off.status === 200 && off.body?.slot === 'off', JSON.stringify(off.body));
check(
'an off photo is on no landing slot',
((await actor().req('/photos')).body?.photos ?? []).find((r) => r.id === id)?.slot === 'off',
'the sections are public, the owner is not',
JSON.stringify(((await actor().req('/photos')).body?.photos ?? []).find((r) => r.id === id)?.slots) ===
JSON.stringify(['tester', 'creator']),
);
const one = await patch(`/admin/photos/${id}`, { slots: ['strip', 'strip', 'qr'] });
check(
'a repeated section is stored once',
JSON.stringify(one.body?.slots) === JSON.stringify(['strip', 'qr']),
JSON.stringify(one.body),
);
// An empty set is the curator's removal: no landing section draws the row,
// but the uploader keeps it in their folder.
const off = await patch(`/admin/photos/${id}`, { slots: [] });
check('an admin takes a photo off the landing', off.status === 200 && off.body?.slots?.length === 0, JSON.stringify(off.body));
check(
'an off photo is on no landing section',
((await actor().req('/photos')).body?.photos ?? []).find((r) => r.id === id)?.slots?.length === 0,
);
check('its owner still has it in the folder', ((await user.req('/photos/mine')).body?.photos ?? []).some((p) => p.id === id));
check('an unknown slot is refused', (await patch(`/admin/photos/${id}`, { slot: 'nope' })).status === 400);
check('placing an unknown photo is a 404', (await patch('/admin/photos/999999', { slot: 'qr' })).status === 404);
check('an unknown slot is refused', (await patch(`/admin/photos/${id}`, { slots: ['nope'] })).status === 400);
check('a bare slot string is refused', (await patch(`/admin/photos/${id}`, { slots: 'strip' })).status === 400);
check('placing an unknown photo is a 404', (await patch('/admin/photos/999999', { slots: ['qr'] })).status === 404);
const deleted = await admin.req(`/admin/photos/${id}`, { method: 'DELETE' });
check('an admin deletes a contribution', deleted.status === 204, `got ${deleted.status}`);