diff --git a/docker/frontend/src/Landing.tsx b/docker/frontend/src/Landing.tsx index bc20244..05a8a0c 100644 --- a/docker/frontend/src/Landing.tsx +++ b/docker/frontend/src/Landing.tsx @@ -82,6 +82,28 @@ function Shot({ src, alt, filter }: { src: string; alt: string; filter?: string ); } +// The ‹ › pair that steps a preview through the stills the curator tagged for +// that slot, wrapping at both ends. A slot with one frame — or none — gets no +// arrows: there is nowhere to step. +function FrameArrows({ count, id, labels, onStep }: { + count: number; + id: string; + labels: { prev: string; next: string }; + onStep: (delta: number) => void; +}) { + if (count < 2) return null; + return ( + <> + + + + ); +} + interface Look { tag: string; title: Txt; @@ -186,6 +208,8 @@ export function Landing() { const account = useRef(null); const [presetId, setPresetId] = useState(PRESETS[0].id); const [testerIdx, setTesterIdx] = useState(0); + const [creatorIdx, setCreatorIdx] = useState(0); + const [qrIdx, setQrIdx] = useState(0); const [temp, setTemp] = useState(0); const [sat, setSat] = useState(100); const [con, setCon] = useState(100); @@ -293,37 +317,38 @@ export function Landing() { [photos], ); - // The three live sections. One random photo out of each set, drawn once per - // page load so the preview does not change under the visitor's eyes on every - // render; a section whose slot holds nothing simply shows no frame. A photo - // the curator put in several sections can turn up in each of them. + // The three live sections, each cycling the stills the curator tagged for its + // slot. Every set is drawn from a random start once per page load, so the + // preview does not change under the visitor's eyes on every render; a slot + // that holds nothing simply shows no frame. A photo the curator put in + // several slots can turn up in each of them. const slotPhoto = useMemo(() => { - const pick = (slot: PhotoSlot) => { - const list = photos.filter((p) => p.slots.includes(slot)); - return list.length ? list[Math.floor(Math.random() * list.length)] : null; - }; - const src = (slot: PhotoSlot) => { - const p = pick(slot); - return p ? api.photoUrl(p.id) : null; - }; + const urls = (slot: PhotoSlot) => photos.filter((p) => p.slots.includes(slot)).map((p) => api.photoUrl(p.id)); + // A plain start index on top of the whole set keeps the frame random per + // load without an effect that would reset it on every re-render. + const start = (n: number) => (n ? Math.floor(Math.random() * n) : 0); // The QR card's payload is the curated photo's own look, as the link the // phone app downloads it from. - // The tester cycles every still the curator put in its slot, so a recipe - // can be checked against more than one frame. - const curated = photos.filter((p) => p.slots.includes('tester')).map((p) => api.photoUrl(p.id)); - const testers = curated; - const qr = pick('qr'); + const qrs = photos.filter((p) => p.slots.includes('qr')).map((p) => ({ + url: api.photoUrl(p.id), + link: p.hasPreset ? new URL(api.presetUrl(p.id), window.location.origin).href : `${window.location.origin}/app`, + })); + const testers = urls('tester'); + const creators = urls('creator'); return { testers, - // A plain step count on top of one random start keeps the frame random - // per load without an effect that would reset it on every re-render. - testerStart: Math.floor(Math.random() * testers.length), - creator: src('creator'), - qr: qr ? api.photoUrl(qr.id) : null, - qrLink: qr?.hasPreset ? new URL(api.presetUrl(qr.id), window.location.origin).href : `${window.location.origin}/app`, + testerStart: start(testers.length), + creators, + creatorStart: start(creators.length), + qrs, + qrStart: start(qrs.length), }; }, [photos]); + // The QR card cycles its stills the way the previews do, but the payload has + // to follow: the card is rendered from the one on screen. + const curQr = slotPhoto.qrs.length ? slotPhoto.qrs[(slotPhoto.qrStart + qrIdx) % slotPhoto.qrs.length] : null; + // The theme popover closes on a click anywhere else, like the workspace's. useEffect(() => { if (!themeMenu) return; @@ -709,28 +734,12 @@ export function Landing() { filter={preset.filter} /> )} - {slotPhoto.testers.length > 1 && ( - <> - - - - )} + setTesterIdx((i) => (i + d + slotPhoto.testers.length) % slotPhoto.testers.length)} + />
f/1.8 SS 1/500s @@ -767,13 +776,19 @@ export function Landing() {
- {slotPhoto.creator && ( + {slotPhoto.creators.length > 0 && ( )} + setCreatorIdx((i) => (i + d + slotPhoto.creators.length) % slotPhoto.creators.length)} + />
{c({ en: 'CUSTOM RECIPE', vi: 'CÔNG THỨC RIÊNG' })}TEMP {temp > 0 ? `+${temp}` : temp}SAT {sat}%CON {con}%
@@ -832,19 +847,25 @@ export function Landing() {
- {slotPhoto.qr && ( + {curQr && ( p.id === 'sunset') ?? PRESETS[0]).filter} /> )}
- +
SUNSET GLOW