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 (
);
}
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 (
{
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 ? (
void onDownload()}
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-3 text-xs font-semibold text-[#333] hover:bg-[#fafafa]"
>
Download
) : null}
{showShare ? (
useNativeShare ? (
void onNativeShare()}
className="inline-flex h-9 w-full items-center justify-center gap-2 rounded-lg border border-[#ddd] bg-white px-3 text-xs font-semibold text-[#333] hover:bg-[#fafafa]"
>
Share
) : (
{enabledSharePlatforms.map((item) => (
void onShare(item.key)}
className="inline-flex h-8 items-center justify-center gap-1 rounded-lg border border-[#ddd] bg-white px-2 text-[11px] font-semibold text-[#333] hover:bg-[#fafafa]"
>
{item.label}
))}
)
) : 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.
void goLogin()}
className="inline-flex h-11 w-full items-center justify-center rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c]"
>
Sign in to continue
) : (
void onSearch()}
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-[#D32F2F] px-4 text-sm font-semibold text-white hover:bg-[#b71c1c] disabled:opacity-55"
>
{searching ? "Searching…" : "Find photos"}
{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}
)}
);
}