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
+12 -7
View File
@@ -35,7 +35,7 @@ import {
photoFile,
photoPath,
sessionUser,
setPhotoSlot,
setPhotoSlots,
setPhotoConsent,
setUserAvatar,
setUserBlocked,
@@ -49,6 +49,7 @@ import {
type Recipe,
type User,
type EventKind,
type PhotoSlot,
} from './db';
const PORT = Number(process.env.PORT || 3000);
@@ -804,18 +805,22 @@ app.delete('/api/admin/photos', async (req, reply) => {
return reply.status(200).send({ removed: files.length });
});
// Curating: which slot on the landing page this photo is allowed to appear in.
// The landing page picks one at random per slot, so several photos in one slot
// rotate between visits.
// Curating: the landing sections this photo is allowed to appear in. A photo
// may sit in several at once — each section picks one of its own at random per
// visit, so several photos in one section rotate. An empty set takes it off
// the landing without deleting the row.
app.patch<{ Params: { id: string } }>('/api/admin/photos/:id', async (req, reply) => {
const user = admin(req);
if ('status' in user) return reply.status(user.status).send({ error: user.status === 401 ? 'unauthorized' : 'forbidden' });
const id = Number(req.params.id);
if (!Number.isInteger(id) || id <= 0) return reply.status(404).send({ error: 'photo not found' });
const b = bodyOf(req);
if (!b || !isPhotoSlot(b.slot)) return reply.status(400).send({ error: 'invalid slot' });
if (!setPhotoSlot(id, b.slot)) return reply.status(404).send({ error: 'photo not found' });
return reply.status(200).send({ id, slot: b.slot });
const slots = b?.slots;
if (!Array.isArray(slots) || !slots.every((s) => isPhotoSlot(s)))
return reply.status(400).send({ error: 'invalid slots' });
const set: PhotoSlot[] = [...new Set(slots as PhotoSlot[])];
if (!setPhotoSlots(id, set)) return reply.status(404).send({ error: 'photo not found' });
return reply.status(200).send({ id, slots: set });
});
app