web: the QR card hands out the look that made the photo

A photo's landing section can now be the QR card, and that section is the
only one that hands something out: the server writes the photo's own stored
look back as the app's .recipe file, at
GET /api/photos/:id/preset.recipe, for any row the curator ticked into the
qr slot. Nothing new is stored — the file is built from the recipe the
upload already carried, so it works for a photo uploaded by the phone too.

The admin pane grows a fourth checkbox and a fourth row (QR card); the
row draws the download link as a scannable code, and the box is dead for a
photo with no stored look. The landing's QR card now encodes the curated
photo's own link instead of a mock address. The listing exposes
hasPreset, never the recipe itself.
This commit is contained in:
2026-09-18 16:57:46 +07:00
parent a35ecf4f1c
commit 8a889db069
11 changed files with 233 additions and 18 deletions
+7
View File
@@ -50,6 +50,10 @@ export interface Photo {
// The uploader's consent to show this frame on the landing film strip. Own
// rows carry it back; the public reel only returns consented ones anyway.
consent?: boolean;
// Whether the row still carries the look that made it. That look is the QR
// card's payload, so the admin needs to know a photo can hand one out without
// the listing shipping the recipe itself.
hasPreset: boolean;
// The look the photo was saved with, on the owner's own listing only — the
// studio reads it back so a saved photo can be reopened for editing.
recipe?: Recipe | null;
@@ -215,6 +219,9 @@ export const api = {
// The owner's own delete; an admin may pass any id here too.
deletePhoto: (id: number) => call<void>(`/photos/${id}`, { method: 'DELETE' }),
photoUrl: (id: number) => `/api/photos/${id}/file`,
// The look a photo was uploaded with, as the `.recipe` file the app imports on
// a scan. Only answers for a photo the curator ticked into the `qr` section.
presetUrl: (id: number) => `/api/photos/${id}/preset.recipe`,
// Admin only: every account, with how many photos it owns.
adminListUsers: () => call<{ users: AdminUser[] }>('/admin/users'),