api+admin: let the curator take a photo off the landing, not just move it

Uploads still land in the community strip with consent on, by the uploader's own
tick — that default stays. What was missing is the curator's removal: the slot
select only offered the other three live placements, so "off the strip" meant
publishing the photo somewhere else or deleting the uploader's row.

`off` is a fifth slot value. The reel draws slot === 'strip' and each live slot
draws its own, so an `off` photo renders nowhere on the landing, while its owner
still has it in MY PHOTOS.
This commit is contained in:
2026-09-18 15:07:04 +07:00
parent dcea6b926f
commit 97836fde8a
6 changed files with 26 additions and 8 deletions
+4 -2
View File
@@ -19,13 +19,15 @@ import type { MsgKey } from './i18n/vi';
type State = 'loading' | 'guest' | 'forbidden' | 'ready';
type Tab = 'profile' | 'users' | 'pictures' | 'stats';
// The four places a photo can be drawn from. `strip` feeds the community reel;
// each live slot shows one photo of its set, picked at random per page load.
// Where a photo is drawn from. `strip` feeds the community reel and each live
// slot shows one photo of its set, picked at random per page load. `off` is the
// way off the landing without deleting the uploader's row.
const SLOTS: { id: PhotoSlot; key: MsgKey }[] = [
{ id: 'strip', key: 'adm.slotStrip' },
{ id: 'tester', key: 'adm.slotTester' },
{ id: 'creator', key: 'adm.slotCreator' },
{ id: 'qr', key: 'adm.slotQr' },
{ id: 'off', key: 'adm.slotOff' },
];
// The left column, in the order it reads.
+3 -2
View File
@@ -32,8 +32,9 @@ 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.
export type PhotoSlot = 'strip' | 'tester' | 'creator' | 'qr';
// 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';
// 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
+2 -1
View File
@@ -166,7 +166,8 @@ export const en: Dict = {
'adm.slotTester': 'Live tester (lp-tester)',
'adm.slotCreator': 'Live recipe section (lp-sec)',
'adm.slotQr': 'QR card (lp-qr)',
'adm.slotHint': 'The three live slots each draw one photo out of their set at random, so several photos in the same slot rotate between visits.',
'adm.slotOff': 'Not on the landing page',
'adm.slotHint': 'The three live slots each draw one photo out of their set at random, so several photos in the same slot rotate between visits. "Not on the landing page" takes a photo off the reel without deleting it — its owner keeps it in MY PHOTOS.',
'adm.signIn': 'Sign in with an admin account',
'adm.notAdmin': 'This account has no admin rights.',
'adm.empty': 'No contributions yet.',
+2 -1
View File
@@ -170,7 +170,8 @@ export const vi = {
'adm.slotTester': 'Live tester (lp-tester)',
'adm.slotCreator': 'Mục công thức live (lp-sec)',
'adm.slotQr': 'Thẻ QR (lp-qr)',
'adm.slotHint': 'Ba vị trí live mỗi lần vào trang sẽ bốc ngẫu nhiên một ảnh trong nhóm, nên nhiều ảnh cùng một vị trí sẽ luân phiên nhau.',
'adm.slotOff': 'Không hiện trên landing page',
'adm.slotHint': 'Ba slot trực tiếp mỗi cái bốc ngẫu nhiên một ảnh trong nhóm của nó, nên nhiều ảnh cùng slot sẽ luân phiên giữa các lần ghé. "Không hiện trên landing page" gỡ ảnh khỏi landing mà không xoá — chủ ảnh vẫn giữ nó trong MY PHOTOS.',
'adm.signIn': 'Đăng nhập bằng tài khoản quản trị',
'adm.notAdmin': 'Tài khoản này không có quyền quản trị.',
'adm.empty': 'Chưa có ảnh đóng góp nào.',