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:
@@ -19,11 +19,10 @@ import type { MsgKey } from './i18n/vi';
|
||||
type State = 'loading' | 'guest' | 'forbidden' | 'ready';
|
||||
type Tab = 'profile' | 'users' | 'pictures' | 'stats';
|
||||
|
||||
// Where a photo can be put. One destination each — the landing page draws the
|
||||
// film strip from `strip` and one random photo per visit out of each live set.
|
||||
// Clicking the destination a photo is already in takes it off the landing
|
||||
// without deleting the uploader's row (slot `off`), which is the same switch
|
||||
// the old picker carried.
|
||||
// Where a photo can be put. The boxes are independent — a photo may sit in all
|
||||
// three sections at once, and each section draws one random photo per visit out
|
||||
// of its own set. Unticking every box takes the photo off the landing without
|
||||
// deleting the uploader's row, which is the same switch the old picker carried.
|
||||
const DESTINATIONS: { id: PhotoSlot; key: MsgKey }[] = [
|
||||
{ id: 'strip', key: 'adm.pickStrip' },
|
||||
{ id: 'tester', key: 'adm.pickTester' },
|
||||
@@ -102,10 +101,21 @@ export function Admin() {
|
||||
await refreshPhotos();
|
||||
});
|
||||
|
||||
const setSlot = (id: number, slot: PhotoSlot) =>
|
||||
// One checkbox tick: the whole new set goes to the API in one call. The box
|
||||
// flips under the curator's finger first — waiting for the round trip would
|
||||
// snap it back for a frame and read as a lost click — and a failed write puts
|
||||
// the old set back.
|
||||
const setSlots = (id: number, next: PhotoSlot[]) =>
|
||||
run(async () => {
|
||||
await api.adminSetPhotoSlot(id, slot);
|
||||
setPhotos((prev) => prev.map((p) => (p.id === id ? { ...p, slot } : p)));
|
||||
const before = photos.find((p) => p.id === id)?.slots ?? [];
|
||||
const show = (slots: PhotoSlot[]) => setPhotos((prev) => prev.map((p) => (p.id === id ? { ...p, slots } : p)));
|
||||
show(next);
|
||||
try {
|
||||
await api.adminSetPhotoSlots(id, next);
|
||||
} catch (err) {
|
||||
show(before);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
const clear = () => {
|
||||
@@ -263,21 +273,28 @@ export function Admin() {
|
||||
<span>{t('adm.uploaded')}: {new Date(p.createdAt).toLocaleString()}</span>
|
||||
<span>{t('adm.size')}: {Math.round(p.bytes / 1024)} KB · {p.mime}</span>
|
||||
</div>
|
||||
{/* One button per destination. The one the photo is
|
||||
already in reads pressed; pressing it sets `off`. */}
|
||||
{/* One checkbox per destination, all three on one
|
||||
line: tick as many as the photo should appear in,
|
||||
untick the last one to take it off the landing. */}
|
||||
<div className="adm-choices">
|
||||
{DESTINATIONS.map((d) => (
|
||||
<button
|
||||
key={d.id}
|
||||
type="button"
|
||||
className={`adm-choice${p.slot === d.id ? ' on' : ''}`}
|
||||
data-key={`adm-pick-${d.id}-${p.id}`}
|
||||
aria-pressed={p.slot === d.id}
|
||||
disabled={busy}
|
||||
onClick={() => void setSlot(p.id, p.slot === d.id ? 'off' : d.id)}
|
||||
>
|
||||
<label className="adm-choice" key={d.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-key={`adm-pick-${d.id}-${p.id}`}
|
||||
checked={p.slots.includes(d.id)}
|
||||
disabled={busy}
|
||||
onChange={() =>
|
||||
void setSlots(
|
||||
p.id,
|
||||
p.slots.includes(d.id)
|
||||
? p.slots.filter((x) => x !== d.id)
|
||||
: [...p.slots, d.id],
|
||||
)
|
||||
}
|
||||
/>
|
||||
{t(d.key)}
|
||||
</button>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
@@ -295,7 +312,7 @@ export function Admin() {
|
||||
</div>
|
||||
<div className="adm-col">
|
||||
{DESTINATIONS.map((d) => {
|
||||
const inRow = shown.filter((p) => p.slot === d.id);
|
||||
const inRow = shown.filter((p) => p.slots.includes(d.id));
|
||||
return (
|
||||
<section className="adm-row" key={d.id} data-key={`adm-row-${d.id}`}>
|
||||
<h3 className="adm-row-title">
|
||||
|
||||
Reference in New Issue
Block a user