import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent, type ReactNode, } from "react"; import { Download, ImagePlus, Share2, } from "lucide-react"; import { useParams } from "react-router-dom"; import { AuthShell } from "../components/auth/AuthShell"; import { PhotoWallCropModal } from "../components/PhotoWallCropModal"; import { PhotoWallEventHeader } from "../components/PhotoWallEventHeader"; import { SharePlatformIcon } from "../components/SharePlatformIcon"; import { getPhotoWallUploadMeta, getVisitorLoginPreview, uploadPhotoWallSelfie, type PhotoWallSharePlatforms, type PhotoWallPublicFormField, type PhotoWallUploadMeta, } from "../lib/api"; import { getAttendeeParticipant, getAttendeeToken } from "../lib/attendeeAuth"; import { getImageNaturalSize, ratiosMatch } from "../lib/cropImage"; import { canUseNativeShareSheet, downloadImageFromUrl, fetchImageFile, ensureShareableImageFile, sharePhotoWallImage, shareViaNativeSheet, } from "../lib/photoWallShare"; import { EB_VISITOR_CONTEXT_KEY, setPostAuthReturnPath, type EbVisitorContext, } from "../types/visitorContext"; function fieldKey(f: PhotoWallPublicFormField): string { return String(f.name || f.label || "").trim(); } function WhiteCard({ children }: { children: ReactNode }) { return (
{children}
); } type CropSession = { imageSrc: string; sourceFileName: string; }; type UploadDoneState = { awaitingApproval: boolean; previewUrl: string | null; /** CORS-safe API URL for download/share */ fileUrl: string | null; framed: boolean; }; const SHARE_LABELS: Array<{ key: keyof PhotoWallSharePlatforms; label: string }> = [ { key: "linkedin", label: "LinkedIn" }, { key: "facebook", label: "Facebook" }, { key: "instagram", label: "Instagram" }, { key: "snapchat", label: "Snapchat" }, { key: "x", label: "X" }, { key: "whatsapp", label: "WhatsApp" }, ]; export function PhotoWallUploadPage() { const { uploadToken } = useParams<{ uploadToken: string }>(); const fileRef = useRef(null); const resultImgRef = useRef(null); const [meta, setMeta] = useState(null); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [file, setFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [values, setValues] = useState>({}); const [uploaderName, setUploaderName] = useState(""); const [submitting, setSubmitting] = useState(false); const [picking, setPicking] = useState(false); const [done, setDone] = useState(null); const [actionMsg, setActionMsg] = useState(null); const [cropSession, setCropSession] = useState(null); /** Cached bytes for share/download — avoids CORS/network fail when opening the OS sheet. */ const [shareFile, setShareFile] = useState(null); const load = useCallback(async () => { if (!uploadToken) return; setLoading(true); setErr(null); try { const data = await getPhotoWallUploadMeta(uploadToken); setMeta(data); } catch (e) { setMeta(null); setErr(e instanceof Error ? e.message : "Photo wall not available"); } finally { setLoading(false); } }, [uploadToken]); useEffect(() => { void load(); }, [load]); useEffect(() => { return () => { if (previewUrl) URL.revokeObjectURL(previewUrl); }; }, [previewUrl]); useEffect(() => { return () => { if (cropSession?.imageSrc) URL.revokeObjectURL(cropSession.imageSrc); }; }, [cropSession]); const fields = useMemo( () => (meta?.form?.fields || []).filter((f) => f.label?.trim() && f.type !== "section"), [meta], ); const loggedIn = Boolean(getAttendeeToken() && getAttendeeParticipant()); const enabledSharePlatforms = useMemo( () => SHARE_LABELS.filter((item) => Boolean(meta?.sharePlatforms?.[item.key])), [meta?.sharePlatforms], ); const useNativeShare = useMemo(() => canUseNativeShareSheet(), []); const clearSelectedPhoto = useCallback(() => { setFile(null); setPreviewUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); if (fileRef.current) fileRef.current.value = ""; }, []); useEffect(() => { if (!done?.fileUrl) return; let cancelled = false; void fetchImageFile(done.fileUrl, "photo-wall.jpg") .then((f) => { if (!cancelled) setShareFile(f); }) .catch(() => { /* keep any local shareFile from upload */ }); return () => { cancelled = true; }; }, [done?.fileUrl]); const resetForNewUpload = useCallback(() => { setDone(null); setShareFile(null); setActionMsg(null); setErr(null); clearSelectedPhoto(); }, [clearSelectedPhoto]); const applySelectedFile = useCallback((next: File) => { setFile(next); setPreviewUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return URL.createObjectURL(next); }); }, []); const handleFileChange = async (selected: File | null) => { if (!selected) { clearSelectedPhoto(); return; } if (!selected.type.startsWith("image/")) { setErr("Please choose an image file"); return; } setErr(null); setPicking(true); try { const requireCrop = Boolean(meta?.requireUploadCrop); if (!requireCrop) { applySelectedFile(selected); return; } const maxW = Math.max(1, Number(meta?.maxPhotoWidth) || 1080); const maxH = Math.max(1, Number(meta?.maxPhotoHeight) || 1080); const natural = await getImageNaturalSize(selected); if (ratiosMatch(natural.width, natural.height, maxW, maxH)) { applySelectedFile(selected); return; } const src = URL.createObjectURL(selected); setCropSession({ imageSrc: src, sourceFileName: selected.name || "photo.jpg" }); } catch (e) { setErr(e instanceof Error ? e.message : "Could not process image"); } finally { setPicking(false); if (fileRef.current) fileRef.current.value = ""; } }; const onSubmit = async (e: FormEvent) => { e.preventDefault(); if (!uploadToken || !file || !meta) return; if (meta.askUploaderName && !uploaderName.trim()) { setErr("Please enter your name"); return; } setSubmitting(true); setErr(null); setActionMsg(null); try { const localFile = file; const result = await uploadPhotoWallSelfie(uploadToken, file, values, { displayName: meta.askUploaderName ? uploaderName.trim() : undefined, }); // Keep local bytes immediately so Share works even if the API file proxy fails (CORS/network). setShareFile(localFile); setDone({ awaitingApproval: result.awaitingApproval, previewUrl: result.previewUrl, fileUrl: result.fileUrl, framed: result.framed, }); clearSelectedPhoto(); // Normalize HEIC/etc. in the background so iOS accepts the file in the OS share sheet. void ensureShareableImageFile(localFile, "photo-wall.jpg").then((shareable) => { setShareFile((prev) => { // Don't clobber a framed file already prefetched from the API. if (prev && prev !== localFile) return prev; return shareable; }); }); } catch (e2) { setErr(e2 instanceof Error ? e2.message : "Upload failed"); } finally { setSubmitting(false); } }; const onDownload = async () => { setActionMsg(null); try { if (shareFile) { const objectUrl = URL.createObjectURL(shareFile); const a = document.createElement("a"); a.href = objectUrl; a.download = shareFile.name || "photo-wall.jpg"; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(objectUrl); setActionMsg("Download started"); return; } const src = done?.fileUrl || done?.previewUrl; if (!src) return; await downloadImageFromUrl(src, "photo-wall.jpg"); setActionMsg("Download started"); } catch (e) { setActionMsg(e instanceof Error ? e.message : "Download failed"); } }; const shareCaption = `Check out my photo from ${meta?.eventName || "the event"}!`; const onNativeShare = async () => { const src = done?.fileUrl || done?.previewUrl; setActionMsg(null); try { // Pass cached File first so share runs without a prior network await (iOS gesture). const result = await shareViaNativeSheet({ file: shareFile, imageUrl: src, imgEl: resultImgRef.current, title: meta?.eventName || "Photo wall", text: shareCaption, }); if (result === "shared") setActionMsg("Shared"); else setActionMsg(null); } catch (e) { const msg = e instanceof Error ? e.message : "Share failed"; setActionMsg( /load failed|failed to fetch/i.test(msg) ? "Could not share this photo. Try Download, then share from your photos." : msg, ); } }; const onShare = async (platform: keyof PhotoWallSharePlatforms) => { const src = done?.fileUrl || done?.previewUrl; if (!src && !shareFile) return; setActionMsg(null); try { const result = await sharePhotoWallImage({ platform, imageUrl: src || "", file: shareFile, title: meta?.eventName || "Photo wall", text: shareCaption, }); if (result === "shared") { setActionMsg( platform === "whatsapp" || platform === "facebook" || platform === "x" || platform === "linkedin" ? "Opening app with your photo link" : "Shared", ); } else if (result === "cancelled") { setActionMsg(null); } else { setActionMsg("Photo saved and caption copied — paste / attach it in the app"); } } catch (e) { setActionMsg(e instanceof Error ? e.message : "Share failed"); } }; const goLogin = async () => { if (!meta?.eventId) return; try { const preview = await getVisitorLoginPreview(meta.eventId); const ctx: EbVisitorContext = { type: "event", eventId: meta.eventId, event: preview, fetchedAt: new Date().toISOString(), }; localStorage.setItem(EB_VISITOR_CONTEXT_KEY, JSON.stringify(ctx)); setPostAuthReturnPath(`/photo-wall/${uploadToken}`); window.location.assign(`/login?type=event&id=${encodeURIComponent(meta.eventId)}`); } catch { setErr("Could not open login"); } }; if (!uploadToken) { return (

Missing upload link

); } if (loading) { return (

Loading…

); } if (err && !meta) { return (

{err}

); } if (done) { const canDownload = Boolean(meta?.allowFramedDownload && (done.fileUrl || done.previewUrl)); const showShare = Boolean((done.fileUrl || done.previewUrl) && enabledSharePlatforms.length > 0); return ( {meta?.event ? (
) : null}

{done.framed ? "Your framed photo" : "Photo uploaded"}

{done.awaitingApproval ? "Thanks! Your photo is waiting for approval." : done.framed ? "Here’s your generated photo. You can edit, download, or share it." : "Thanks! Your photo was added to the wall."}

{(done.fileUrl || done.previewUrl) ? (
{done.framed { const img = e.currentTarget; // Retry without CORS mode if CDN blocks anonymous reads. if (img.crossOrigin) { img.crossOrigin = null; img.src = done.previewUrl || done.fileUrl || img.src; return; } if (done.fileUrl && img.src !== done.fileUrl) { img.src = done.fileUrl; } }} />
) : null}
{canDownload ? ( ) : null}
{showShare ? (

Share

{useNativeShare ? ( ) : (
{enabledSharePlatforms.map((item) => ( ))}
)}
) : null} {actionMsg ?

{actionMsg}

: null}
); } return ( {meta?.event ? (
) : null}

Upload your photo

{meta?.form?.description || "Add a selfie to the event photo wall."}

{meta?.accessMode === "login" && !loggedIn ? (

Sign in to upload your photo for this event.

) : (
void onSubmit(e)} className="space-y-4">
void handleFileChange(e.target.files?.[0] || null)} /> {file ? (

{file.name}

) : null}
{meta?.askUploaderName ? ( ) : null} {fields.map((f) => { const key = fieldKey(f); if (!key) return null; const required = Boolean(f.required); if (f.type === "textarea") { return (