Photo wall Presenter changes

This commit is contained in:
Ravi Gulhane 2026-08-18 15:30:02 +05:30
commit 77c77ff9db
6 changed files with 122 additions and 69 deletions

View File

@ -49,7 +49,7 @@ export function PhotoWallCropModal({
return ( return (
<div className="fixed inset-0 z-50 flex items-end justify-center bg-black/70 p-4 sm:items-center"> <div className="fixed inset-0 z-50 flex items-end justify-center bg-black/70 p-4 sm:items-center">
<div className="flex w-full max-w-[420px] flex-col overflow-hidden rounded-2xl card-bg shadow-xl"> <div className="flex w-full max-w-[420px] flex-col overflow-hidden rounded-2xl card-bg shadow-xl">
<div className="border-b border-[var(--theme-card-border-color)]/5 px-4 py-3"> <div className="border-b border-[var(--theme-card-text-color)]/10 px-4 py-3">
<p className="font-inter text-base font-semibold text-[var(--theme-card-text-color)]">Crop your photo</p> <p className="font-inter text-base font-semibold text-[var(--theme-card-text-color)]">Crop your photo</p>
<p className="mt-0.5 font-inter text-xs text-[var(--theme-card-text-color)]/80"> <p className="mt-0.5 font-inter text-xs text-[var(--theme-card-text-color)]/80">
Adjust to match the wall photo ratio before uploading. Adjust to match the wall photo ratio before uploading.

View File

@ -260,7 +260,6 @@ export function LoginPage() {
<div className="min-w-0 space-y-1"> <div className="min-w-0 space-y-1">
<p className="text-xl font-bold leading-tight text-[var(--theme-card-text-color)] sm:text-2xl"> <p className="text-xl font-bold leading-tight text-[var(--theme-card-text-color)] sm:text-2xl">
{headline} {headline}
{/* Novel Lung Health Summit 2026 Kochi 22nd & 23rd August 2026 */}
</p> </p>
{preview?.subtitle ? ( {preview?.subtitle ? (
<p className="text-sm font-medium text-[var(--theme-card-text-color)]/80">{preview.subtitle}</p> <p className="text-sm font-medium text-[var(--theme-card-text-color)]/80">{preview.subtitle}</p>

View File

@ -191,7 +191,7 @@ function MatchCard({
<button <button
type="button" type="button"
onClick={() => void onNativeShare()} onClick={() => void onNativeShare()}
className="inline-flex h-[34px] w-[34px] items-center justify-center rounded-lg border-0 border-[var(--theme-card-text-color)]/45 bg-[var(--theme-card-color)]/80 text-xs font-semibold text-[var(--theme-card-text-color)]" className="inline-flex h-[34px] w-[34px] items-center justify-center rounded-lg border-0 border-[var(--theme-card-text-color)]/45 bg-[var(--theme-card-text-color)]/50 text-xs font-semibold text-[var(--theme-card-text-color)]"
> >
<Share2 className="size-5" /> <Share2 className="size-5" />
</button> </button>
@ -202,7 +202,7 @@ function MatchCard({
key={item.key} key={item.key}
type="button" type="button"
onClick={() => void onShare(item.key)} onClick={() => void onShare(item.key)}
className="inline-flex h-[34px] w-[34px] items-center justify-center rounded-lg border-0 border-[var(--theme-card-text-color)]/45 bg-[var(--theme-card-color)]/80 text-xs font-semibold text-[var(--theme-card-text-color)]" className="inline-flex h-[34px] w-[34px] items-center justify-center rounded-lg border-0 border-[var(--theme-card-text-color)]/45 bg-[var(--theme-card-text-color)]/50 text-xs font-semibold text-[var(--theme-card-text-color)]"
> >
<SharePlatformIcon platform={item.key} /> <SharePlatformIcon platform={item.key} />
{/* {item.label} */} {/* {item.label} */}

View File

@ -101,13 +101,13 @@ export function PhotoWallPresentPage() {
{data.photos.map((p) => ( {data.photos.map((p) => (
<div <div
key={p._id} key={p._id}
className="flex w-full flex-col items-center" className="flex w-full flex-col items-center rounded-xl bg-[var(--theme-card-color)]/50"
style={{ maxWidth: maxW }} style={{ maxWidth: maxW }}
> >
<img <img
src={p.url} src={p.url}
alt="" alt={p.displayName || "Photo Wall"}
className="w-full rounded-md object-cover shadow-lg" className="relative z-1 w-full object-cover shadow-lg"
style={{ style={{
maxWidth: maxW, maxWidth: maxW,
maxHeight: maxH, maxHeight: maxH,
@ -117,7 +117,7 @@ export function PhotoWallPresentPage() {
/> />
{showNames && p.displayName ? ( {showNames && p.displayName ? (
<p <p
className="mt- w-full truncate rounded-md bg-[var(--theme-card-color)] p-1 text-center text-xs font-medium text-[var(--theme-card-text-color)]/90 sm:text-sm" className="mt-1 w-full truncate rounded-md p-2 text-center text-sm font-bold text-[var(--theme-card-text-color)]/90"
title={p.displayName} title={p.displayName}
> >
{p.displayName} {p.displayName}

View File

@ -10,12 +10,14 @@ import {
import { import {
Download, Download,
ImagePlus, ImagePlus,
Loader2,
Share2, Share2,
} from "lucide-react"; } from "lucide-react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { AuthShell } from "../components/auth/AuthShell"; import { HeaderMobile } from "../components/HeaderMobile";
import { MobileShell } from "../components/MobileShell";
import { PhotoWallCropModal } from "../components/PhotoWallCropModal"; import { PhotoWallCropModal } from "../components/PhotoWallCropModal";
import { PhotoWallEventHeader } from "../components/PhotoWallEventHeader"; import { PublicEventChrome } from "../components/PublicEventChrome";
import { SharePlatformIcon } from "../components/SharePlatformIcon"; import { SharePlatformIcon } from "../components/SharePlatformIcon";
import { import {
getPhotoWallUploadMeta, getPhotoWallUploadMeta,
@ -45,13 +47,29 @@ function fieldKey(f: PhotoWallPublicFormField): string {
return String(f.name || f.label || "").trim(); return String(f.name || f.label || "").trim();
} }
function WhiteCard({ children }: { children: ReactNode }) { function PhotoWallLayout({
children,
meta,
}: {
children: ReactNode;
meta?: PhotoWallUploadMeta | null;
}) {
return ( return (
<div className="flex w-full max-w-[354px] flex-col items-center"> <MobileShell className="relative bg-transparent">
<div className="w-full rounded-xl shadow-[0_8px_12px_6px_rgba(0,0,0,0.15),0_4px_4px_rgba(0,0,0,0.3)]"> {meta ? (
<div className="rounded-xl card-bg text-[var(--theme-card-text-color)] shadow-md px-6 py-6">{children}</div> <PublicEventChrome
</div> event={meta.event}
</div> eventHeader={meta.eventHeader}
eventNameFallback={meta.eventName}
eventId={meta.eventId}
/>
) : (
<HeaderMobile showRewardsAndLogout={false} showBack={false} />
)}
<main className="relative z-0 flex w-full max-w-none flex-1 flex-col pb-8 pt-6">
<div className="flex w-full max-w-none flex-col px-4">{children}</div>
</main>
</MobileShell>
); );
} }
@ -91,6 +109,7 @@ export function PhotoWallUploadPage() {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [picking, setPicking] = useState(false); const [picking, setPicking] = useState(false);
const [done, setDone] = useState<UploadDoneState | null>(null); const [done, setDone] = useState<UploadDoneState | null>(null);
const [resultImageReady, setResultImageReady] = useState(false);
const [actionMsg, setActionMsg] = useState<string | null>(null); const [actionMsg, setActionMsg] = useState<string | null>(null);
const [cropSession, setCropSession] = useState<CropSession | null>(null); const [cropSession, setCropSession] = useState<CropSession | null>(null);
/** Cached bytes for share/download — avoids CORS/network fail when opening the OS sheet. */ /** Cached bytes for share/download — avoids CORS/network fail when opening the OS sheet. */
@ -148,6 +167,15 @@ export function PhotoWallUploadPage() {
if (fileRef.current) fileRef.current.value = ""; if (fileRef.current) fileRef.current.value = "";
}, []); }, []);
useEffect(() => {
const src = done?.previewUrl || done?.fileUrl;
if (!done || !src) return;
const img = resultImgRef.current;
if (img?.complete && img.naturalWidth > 0) {
setResultImageReady(true);
}
}, [done]);
useEffect(() => { useEffect(() => {
if (!done?.fileUrl) return; if (!done?.fileUrl) return;
let cancelled = false; let cancelled = false;
@ -165,6 +193,7 @@ export function PhotoWallUploadPage() {
const resetForNewUpload = useCallback(() => { const resetForNewUpload = useCallback(() => {
setDone(null); setDone(null);
setResultImageReady(false);
setShareFile(null); setShareFile(null);
setActionMsg(null); setActionMsg(null);
setErr(null); setErr(null);
@ -221,6 +250,7 @@ export function PhotoWallUploadPage() {
return; return;
} }
setSubmitting(true); setSubmitting(true);
setResultImageReady(false);
setErr(null); setErr(null);
setActionMsg(null); setActionMsg(null);
try { try {
@ -350,31 +380,46 @@ export function PhotoWallUploadPage() {
if (!uploadToken) { if (!uploadToken) {
return ( return (
<AuthShell showLogo={false}> <PhotoWallLayout>
<WhiteCard> <p className="text-center text-sm text-red-600">Missing upload link</p>
<p className="text-center text-sm text-red-600">Missing upload link</p> </PhotoWallLayout>
</WhiteCard>
</AuthShell>
); );
} }
if (loading) { if (loading) {
return ( return (
<AuthShell showLogo={false}> <PhotoWallLayout>
<WhiteCard> <p className="text-center text-sm text-[var(--theme-card-text-color)]/80">Loading…</p>
<p className="text-center text-sm text-[var(--theme-card-text-color)]/70">Loading…</p> </PhotoWallLayout>
</WhiteCard>
</AuthShell>
); );
} }
if (err && !meta) { if (err && !meta) {
return ( return (
<AuthShell showLogo={false}> <PhotoWallLayout>
<WhiteCard> <p className="text-center text-sm text-red-600">{err}</p>
<p className="text-center text-sm text-red-600">{err}</p> </PhotoWallLayout>
</WhiteCard> );
</AuthShell> }
if (submitting) {
const framing = Boolean(meta?.frameEnabled);
return (
<PhotoWallLayout meta={meta}>
<section className="w-full card-bg rounded-lg shadow-md p-6">
<div className="flex flex-col items-center py-8 text-center">
<div className="mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-[var(--theme-button-color)]/10">
<Loader2 className="h-8 w-8 animate-spin text-[var(--theme-button-color)]" aria-hidden />
</div>
<h2 className="mb-2 text-[22px] font-semibold text-[var(--theme-card-text-color)]">
{framing ? "Creating your photo" : "Uploading your photo"}
</h2>
<p className="text-sm text-[var(--theme-card-text-color)]/80" aria-live="polite">
{framing ? "Hang tight — we're adding your frame." : "This may take a moment."}
</p>
</div>
</section>
</PhotoWallLayout>
); );
} }
@ -382,13 +427,8 @@ export function PhotoWallUploadPage() {
const canDownload = Boolean(meta?.allowFramedDownload && (done.fileUrl || done.previewUrl)); const canDownload = Boolean(meta?.allowFramedDownload && (done.fileUrl || done.previewUrl));
const showShare = Boolean((done.fileUrl || done.previewUrl) && enabledSharePlatforms.length > 0); const showShare = Boolean((done.fileUrl || done.previewUrl) && enabledSharePlatforms.length > 0);
return ( return (
<AuthShell showLogo={false}> <PhotoWallLayout meta={meta}>
<WhiteCard> <section className="w-full card-bg rounded-lg shadow-md p-6">
{meta?.event ? (
<div className="mb-4">
<PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} />
</div>
) : null}
<h1 className="mb-2 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]"> <h1 className="mb-2 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">
{done.framed ? "Your framed photo" : "Photo uploaded"} {done.framed ? "Your framed photo" : "Photo uploaded"}
</h1> </h1>
@ -396,17 +436,34 @@ export function PhotoWallUploadPage() {
{done.awaitingApproval {done.awaitingApproval
? "Thanks! Your photo is waiting for approval." ? "Thanks! Your photo is waiting for approval."
: done.framed : done.framed
? "Here’s your generated photo. You can edit, download, or share it." ? "Here's your generated photo. You can edit, download, or share it."
: "Thanks! Your photo was added to the wall."} : "Thanks! Your photo was added to the wall."}
</p> </p>
{(done.fileUrl || done.previewUrl) ? ( {(done.fileUrl || done.previewUrl) ? (
<div className="mb-4 overflow-hidden rounded-lg border border-[#e5e5e5] bg-[#f8f8f8]"> <div className="relative mb-4 min-h-[220px] overflow-hidden rounded-lg bg-[var(--theme-card-text-color)]/5">
{!resultImageReady ? (
<div
className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3"
role="status"
aria-live="polite"
aria-label="Loading photo"
>
<div
className="size-10 animate-spin rounded-full border-[3px] border-[var(--theme-button-color)]/25 border-t-[var(--theme-button-color)]"
aria-hidden
/>
<p className="text-xs font-medium text-[var(--theme-card-text-color)]/70">Preparing your photo…</p>
</div>
) : null}
<img <img
ref={resultImgRef} ref={resultImgRef}
crossOrigin="anonymous" crossOrigin="anonymous"
src={done.previewUrl || done.fileUrl || undefined} src={done.previewUrl || done.fileUrl || undefined}
alt={done.framed ? "Your framed photo" : "Uploaded photo"} alt={done.framed ? "Your framed photo" : "Uploaded photo"}
className="mx-auto max-h-[360px] w-full object-contain" className={`mx-auto max-h-[360px] w-full object-contain transition-opacity duration-300 ${
resultImageReady ? "opacity-100" : "opacity-0"
}`}
onLoad={() => setResultImageReady(true)}
onError={(e) => { onError={(e) => {
const img = e.currentTarget; const img = e.currentTarget;
// Retry without CORS mode if CDN blocks anonymous reads. // Retry without CORS mode if CDN blocks anonymous reads.
@ -417,7 +474,9 @@ export function PhotoWallUploadPage() {
} }
if (done.fileUrl && img.src !== done.fileUrl) { if (done.fileUrl && img.src !== done.fileUrl) {
img.src = done.fileUrl; img.src = done.fileUrl;
return;
} }
setResultImageReady(true);
}} }}
/> />
</div> </div>
@ -426,18 +485,18 @@ export function PhotoWallUploadPage() {
<button <button
type="button" type="button"
onClick={resetForNewUpload} onClick={resetForNewUpload}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-[#D32F2F theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg theme-btn-bg px-4 text-sm font-semibold"
> >
<ImagePlus className="h-4 w-4" /> <ImagePlus className="size-5" />
Edit / Upload new photo Edit / Upload new photo
</button> </button>
{canDownload ? ( {canDownload ? (
<button <button
type="button" type="button"
onClick={() => void onDownload()} onClick={() => void onDownload()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 text-gray-600 px-4 text-sm font-semibold"
> >
<Download className="h-4 w-4" /> <Download className="size-5" />
Download photo Download photo
</button> </button>
) : null} ) : null}
@ -451,42 +510,37 @@ export function PhotoWallUploadPage() {
<button <button
type="button" type="button"
onClick={() => void onNativeShare()} onClick={() => void onNativeShare()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-4 text-sm font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 text-gray-600 px-4 text-sm font-semibold"
> >
<Share2 className="h-4 w-4" /> <Share2 className="size-5" />
Share Share
</button> </button>
) : ( ) : (
<div className="grid grid-cols-2 gap-2"> <div className="flex flex-wrap justify-center gap-2">
{enabledSharePlatforms.map((item) => ( {enabledSharePlatforms.map((item) => (
<button <button
key={item.key} key={item.key}
type="button" type="button"
onClick={() => void onShare(item.key)} onClick={() => void onShare(item.key)}
className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-3 text-xs font-semibold text-[#333] hover:bg-[#fafafa]" className="inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-zinc-300 bg-zinc-100 text-gray-600 px-3 text-xs font-semibold"
> >
<SharePlatformIcon platform={item.key} /> <SharePlatformIcon platform={item.key} />
{item.label} {/* {item.label} */}
</button> </button>
))} ))}
</div> </div>
)} )}
</div> </div>
) : null} ) : null}
{actionMsg ? <p className="mt-3 text-center text-xs text-[#666]">{actionMsg}</p> : null} {actionMsg ? <p className="mt-3 text-center text-xs text-[var(--theme-card-text-color)]/80">{actionMsg}</p> : null}
</WhiteCard> </section>
</AuthShell> </PhotoWallLayout>
); );
} }
return ( return (
<AuthShell showLogo={false}> <PhotoWallLayout meta={meta}>
<WhiteCard> <section className="w-full card-bg rounded-lg shadow-md p-6">
{meta?.event ? (
<div className="mb-4">
<PhotoWallEventHeader event={meta.event} display={meta.eventHeader || null} />
</div>
) : null}
<h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Upload your photo</h1> <h1 className="mb-1 text-center text-[22px] font-semibold text-[var(--theme-card-text-color)]">Upload your photo</h1>
<p className="mb-5 text-center text-sm text-[var(--theme-card-text-color)]/80"> <p className="mb-5 text-center text-sm text-[var(--theme-card-text-color)]/80">
{meta?.form?.description || "Add a selfie to the event photo wall."} {meta?.form?.description || "Add a selfie to the event photo wall."}
@ -518,13 +572,13 @@ export function PhotoWallUploadPage() {
type="button" type="button"
disabled={picking} disabled={picking}
onClick={() => fileRef.current?.click()} onClick={() => fileRef.current?.click()}
className="flex w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-[#ccc] bg-[#fafafa] px-4 py-8 text-sm text-[#666] hover:border-[#D32F2F] hover:bg-[#fff5f5]" className="flex w-full flex-col items-center justify-center gap-2 rounded-lg border-2 border-dashed border-[var(--theme-card-text-color)]/30 bg-[var(--theme-card-color)]/80 px-4 py-4 text-sm text-[var(--theme-card-text-color)] hover:border-[var(--theme-button-color)]/70"
> >
{previewUrl ? ( {previewUrl ? (
<img src={previewUrl} alt="Preview" className="max-h-48 rounded-md object-contain" /> <img src={previewUrl} alt="Preview" className="max-h-48 rounded-md object-contain" />
) : ( ) : (
<> <>
<ImagePlus className="h-8 w-8 text-[#D32F2F]" /> <ImagePlus className="size-8 text-[var(--theme-button-color)]" />
<span>{picking ? "Preparing…" : "Tap to take or choose a photo"}</span> <span>{picking ? "Preparing…" : "Tap to take or choose a photo"}</span>
</> </>
)} )}
@ -566,7 +620,7 @@ export function PhotoWallUploadPage() {
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))} onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
required={required} required={required}
rows={3} rows={3}
className="w-full rounded-lg border border-[rgba(116,112,112,0.3)] px-3 py-2 text-sm transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30" className="w-full rounded-lg border border-[var(--theme-card-text-color)]/10 bg-[var(--theme-card-color)]/40 px-3 py-2 text-sm transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30"
/> />
</label> </label>
); );
@ -582,7 +636,7 @@ export function PhotoWallUploadPage() {
value={String(values[key] ?? "")} value={String(values[key] ?? "")}
onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))} onChange={(e) => setValues((v) => ({ ...v, [key]: e.target.value }))}
required={required} required={required}
className="h-10 w-full rounded-lg border border-[rgba(116,112,112,0.3)] px-3 text-sm transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30" className="h-10 w-full rounded-lg border border-[var(--theme-card-text-color)]/10 bg-[var(--theme-card-color)]/40 px-3 text-sm transition-colors duration-300 ease-in-out focus:border-[var(--theme-button-color)] focus:outline-none focus:ring-2 focus:ring-[var(--theme-button-color)]/30"
/> />
</label> </label>
); );
@ -593,13 +647,13 @@ export function PhotoWallUploadPage() {
<button <button
type="submit" type="submit"
disabled={!file || submitting || picking} disabled={!file || submitting || picking}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F theme-btn-bg px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-50" className="inline-flex h-11 w-full items-center justify-center rounded-lg theme-btn-bg px-4 text-sm font-semibold text-white disabled:opacity-50"
> >
{submitting ? "Uploading…" : "Upload"} {submitting ? "Uploading…" : "Upload"}
</button> </button>
</form> </form>
)} )}
</WhiteCard> </section>
{cropSession && meta ? ( {cropSession && meta ? (
<PhotoWallCropModal <PhotoWallCropModal
@ -620,6 +674,6 @@ export function PhotoWallUploadPage() {
}} }}
/> />
) : null} ) : null}
</AuthShell> </PhotoWallLayout>
); );
} }

View File

@ -397,7 +397,7 @@ export function SocialSharePage() {
Here's your framed photo. You can download or share it. Here's your framed photo. You can download or share it.
</p> </p>
{(done.fileUrl || done.previewUrl) ? ( {(done.fileUrl || done.previewUrl) ? (
<div className="relative mb-4 min-h-[220px] overflow-hidden rounded-lg bg-[var(--theme-card-text-color)]/5"> <div className="relative mb-4 min-h-[220px] overflow-hidden rounded-lg">
{!resultImageReady ? ( {!resultImageReady ? (
<div <div
className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3" className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3"
@ -489,7 +489,7 @@ export function SocialSharePage() {
)} )}
</div> </div>
) : null} ) : null}
{actionMsg ? <p className="mt-3 text-center text-xs text-[#666]">{actionMsg}</p> : null} {actionMsg ? <p className="mt-3 text-center text-xs text-[var(--theme-card-text-color)]/80">{actionMsg}</p> : null}
</section> </section>
</SocialShareLayout> </SocialShareLayout>
); );