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:
@@ -4,6 +4,7 @@ import { useI18n } from './i18n/I18nProvider';
|
||||
import { ACCENTS, FONT_SETS, useTheme, type FontSetId } from './theme/ThemeProvider';
|
||||
import { AuthModal } from './ui/AuthModal';
|
||||
import { Avatar } from './ui/Avatar';
|
||||
import { QrCode } from './ui/QrCode';
|
||||
|
||||
// Landing page, built to the landing blueprint: the 10-section funnel. The
|
||||
// marketing prose is bilingual and lives here next to the markup as `Txt`
|
||||
@@ -160,7 +161,6 @@ export function Landing() {
|
||||
const [sat, setSat] = useState(100);
|
||||
const [con, setCon] = useState(100);
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
const [qrOk, setQrOk] = useState(true);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [photos, setPhotos] = useState<Photo[]>([]);
|
||||
// Sign up / log in happens in a dialog on this page, not by a trip to the
|
||||
@@ -236,11 +236,24 @@ export function Landing() {
|
||||
// render; the built-in sample stands in while a section has no curation. A
|
||||
// photo the curator put in several sections can turn up in each of them.
|
||||
const slotPhoto = useMemo(() => {
|
||||
const pick = (slot: PhotoSlot, fallback: string) => {
|
||||
const pick = (slot: PhotoSlot) => {
|
||||
const list = photos.filter((p) => p.slots.includes(slot));
|
||||
return list.length ? api.photoUrl(list[Math.floor(Math.random() * list.length)].id) : fallback;
|
||||
return list.length ? list[Math.floor(Math.random() * list.length)] : null;
|
||||
};
|
||||
const src = (slot: PhotoSlot, fallback: string) => {
|
||||
const p = pick(slot);
|
||||
return p ? api.photoUrl(p.id) : fallback;
|
||||
};
|
||||
// The QR card's payload is the curated photo's own look, as the link the
|
||||
// phone app downloads it from. The card's mock address stands in while the
|
||||
// section holds nothing that has a look to hand out.
|
||||
const qr = pick('qr');
|
||||
return {
|
||||
tester: src('tester', SAMPLE(1043)),
|
||||
creator: src('creator', SAMPLE(1015)),
|
||||
qr: qr ? api.photoUrl(qr.id) : SAMPLE(1069),
|
||||
qrLink: qr?.hasPreset ? new URL(api.presetUrl(qr.id), window.location.origin).href : `${window.location.origin}/app`,
|
||||
};
|
||||
return { tester: pick('tester', SAMPLE(1043)), creator: pick('creator', SAMPLE(1015)), qr: pick('qr', SAMPLE(1069)) };
|
||||
}, [photos]);
|
||||
|
||||
// The theme popover closes on a click anywhere else, like the workspace's.
|
||||
@@ -708,15 +721,7 @@ export function Landing() {
|
||||
</div>
|
||||
<div className="lp-rc-row">
|
||||
<div className="lp-rc-qr">
|
||||
{qrOk ? (
|
||||
<img
|
||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(`${window.location.origin}/app`)}`}
|
||||
alt="QR"
|
||||
loading="lazy"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={() => setQrOk(false)}
|
||||
/>
|
||||
) : null}
|
||||
<QrCode url={slotPhoto.qrLink} />
|
||||
</div>
|
||||
<div className="lp-rc-meta">
|
||||
<b>SUNSET GLOW</b>
|
||||
|
||||
Reference in New Issue
Block a user