import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { Download, Facebook, Instagram, Linkedin, Search, Share2, } from "lucide-react"; import { useParams } from "react-router-dom"; import { AuthShell } from "../components/auth/AuthShell"; import { PhotoWallEventHeader } from "../components/PhotoWallEventHeader"; import { getPhotoSearchMeta, getVisitorLoginPreview, searchPhotoSearchMatches, type PhotoSearchMatch, type PhotoSearchMeta, type PhotoWallSharePlatforms, } from "../lib/api"; import { getAttendeeParticipant, getAttendeeToken } from "../lib/attendeeAuth"; import { canUseNativeShareSheet, downloadImageFromUrl, fetchImageFile, sharePhotoWallImage, shareViaNativeSheet, } from "../lib/photoWallShare"; import { EB_VISITOR_CONTEXT_KEY, setPostAuthReturnPath, type EbVisitorContext, } from "../types/visitorContext"; function WhiteCard({ children, wide }: { children: ReactNode; wide?: boolean }) { return (
{children}
); } 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" }, ]; function ShareIcon({ platform }: { platform: keyof PhotoWallSharePlatforms }) { if (platform === "linkedin") return ; if (platform === "facebook") return ; if (platform === "instagram") return ; return ; } function MatchCard({ match, eventName, allowDownload, showShare, useNativeShare, enabledSharePlatforms, }: { match: PhotoSearchMatch; eventName: string; allowDownload: boolean; showShare: boolean; useNativeShare: boolean; enabledSharePlatforms: Array<{ key: keyof PhotoWallSharePlatforms; label: string }>; }) { const imgRef = useRef(null); const [shareFile, setShareFile] = useState(null); const [actionMsg, setActionMsg] = useState(null); const caption = `Check out my photo from ${eventName}!`; /** Grid preview uses CDN thumb (`url`) only — never the API file proxy. */ const previewSrc = match.url || undefined; const downloadSrc = match.fileUrl || match.url || undefined; useEffect(() => { if (!match.fileUrl) return; let cancelled = false; void fetchImageFile(match.fileUrl, match.originalFileName || "event-photo.jpg") .then((f) => { if (!cancelled) setShareFile(f); }) .catch(() => { /* ignore — share can still try URL / canvas */ }); return () => { cancelled = true; }; }, [match.fileUrl, match.originalFileName]); 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 || "event-photo.jpg"; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(objectUrl); setActionMsg("Download started"); return; } if (!downloadSrc) return; await downloadImageFromUrl(downloadSrc, match.originalFileName || "event-photo.jpg"); setActionMsg("Download started"); } catch (e) { setActionMsg(e instanceof Error ? e.message : "Download failed"); } }; const onNativeShare = async () => { setActionMsg(null); try { const result = await shareViaNativeSheet({ file: shareFile, imageUrl: downloadSrc, imgEl: imgRef.current, title: eventName, text: caption, }); 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) => { if (!downloadSrc && !shareFile) return; setActionMsg(null); try { const result = await sharePhotoWallImage({ platform, imageUrl: downloadSrc || "", file: shareFile, title: eventName, text: caption, }); 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"); } }; return (
{match.originalFileName { const img = e.currentTarget; // Retry without CORS for CDN; do not switch to API file proxy for display. if (img.crossOrigin) { img.crossOrigin = null; if (previewSrc) img.src = previewSrc; } }} />
{allowDownload ? ( ) : null} {showShare ? ( useNativeShare ? ( ) : (
{enabledSharePlatforms.map((item) => ( ))}
) ) : null} {actionMsg ?

{actionMsg}

: null}
); } export function PhotoSearchPage() { const { searchToken } = useParams<{ searchToken: string }>(); const fileRef = 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 [searching, setSearching] = useState(false); const [matches, setMatches] = useState(null); const [searched, setSearched] = useState(false); const load = useCallback(async () => { if (!searchToken) return; setLoading(true); setErr(null); try { const data = await getPhotoSearchMeta(searchToken); setMeta(data); } catch (e) { setMeta(null); setErr(e instanceof Error ? e.message : "Photo search not available"); } finally { setLoading(false); } }, [searchToken]); useEffect(() => { void load(); }, [load]); useEffect(() => { return () => { if (previewUrl) URL.revokeObjectURL(previewUrl); }; }, [previewUrl]); const loggedIn = Boolean(getAttendeeToken() && getAttendeeParticipant()); const enabledSharePlatforms = useMemo( () => SHARE_LABELS.filter((item) => Boolean(meta?.sharePlatforms?.[item.key])), [meta?.sharePlatforms], ); const useNativeShare = useMemo(() => canUseNativeShareSheet(), []); const showShare = enabledSharePlatforms.length > 0; const allowDownload = meta?.allowDownload !== false; const clearFile = () => { setFile(null); setPreviewUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); if (fileRef.current) fileRef.current.value = ""; }; const onPick = (selected: File | null) => { if (!selected) { clearFile(); return; } if (!selected.type.startsWith("image/")) { setErr("Please choose an image file"); return; } setErr(null); setMatches(null); setSearched(false); setFile(selected); setPreviewUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return URL.createObjectURL(selected); }); }; const onSearch = async () => { if (!searchToken || !file || !meta) return; setSearching(true); setErr(null); try { const result = await searchPhotoSearchMatches(searchToken, file); setMatches(result.matches); setSearched(true); } catch (e) { setMatches(null); setSearched(false); setErr(e instanceof Error ? e.message : "Search failed"); } finally { setSearching(false); } }; 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-search/${searchToken}`); window.location.assign(`/login?type=event&id=${encodeURIComponent(meta.eventId)}`); } catch { setErr("Could not open login"); } }; if (!searchToken) { return (

Missing search link

); } if (loading) { return (

Loading…

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

{err}

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

Find Photos

Upload a selfie to find photos of you from {meta?.eventName || "the event"}.

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

Sign in to search for your photos.

) : (
onPick(e.target.files?.[0] || null)} /> {previewUrl ? (
Your selfie
) : null}
{err ?

{err}

: null} {searched ? (

{matches && matches.length > 0 ? `${matches.length} match${matches.length === 1 ? "" : "es"}` : "No matches found"}

{matches && matches.length > 0 ? (
{matches.map((m) => ( ))}
) : null}
) : null}
)}
); }