import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import { Download, Share2, ArrowLeft, } from "lucide-react"; import { useParams } from "react-router-dom"; import { HeaderMobile } from "../components/HeaderMobile"; import { MobileShell } from "../components/MobileShell"; import { PhotoSourcePicker } from "../components/PhotoSourcePicker"; import { PhotoWallCropModal } from "../components/PhotoWallCropModal"; import { PublicEventChrome } from "../components/PublicEventChrome"; import { SharePlatformIcon } from "../components/SharePlatformIcon"; import { getSocialShareMeta, uploadSocialSharePhoto, type PhotoWallSharePlatforms, type SocialShareMeta, type SocialSharePublicFrame, } from "../lib/api"; import { getImageNaturalSize, ratiosMatch } from "../lib/cropImage"; import { canUseNativeShareSheet, downloadImageFromUrl, ensureShareableImageFile, fetchImageFile, sharePhotoWallImage, shareViaNativeSheet, } from "../lib/photoWallShare"; import { useActivityProfileGate } from "../lib/useActivityProfileGate"; function SocialShareLayout({ children, meta, }: { children: ReactNode; meta?: SocialShareMeta | null; }) { return ( {meta ? ( ) : ( )}
{children}
); } type CropSession = { imageSrc: string; sourceFileName: string; }; type DoneState = { previewUrl: string | null; fileUrl: string | null; }; 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 SocialSharePage() { const { uploadToken } = useParams<{ uploadToken: string }>(); const resultImgRef = useRef(null); const [meta, setMeta] = useState(null); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [selectedFrame, setSelectedFrame] = useState(null); const [displayName, setDisplayName] = useState(""); const [file, setFile] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [submitting, setSubmitting] = useState(false); const [picking, setPicking] = useState(false); const [done, setDone] = useState(null); const [resultImageReady, setResultImageReady] = useState(false); const [actionMsg, setActionMsg] = useState(null); const [cropSession, setCropSession] = useState(null); const [shareFile, setShareFile] = useState(null); const load = useCallback(async () => { if (!uploadToken) return; setLoading(true); setErr(null); try { const data = await getSocialShareMeta(uploadToken); setMeta(data); } catch (e) { setMeta(null); setErr(e instanceof Error ? e.message : "Social share 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]); 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(() => { if (!done?.fileUrl) return; let cancelled = false; void fetchImageFile(done.fileUrl, "social-share.jpg") .then((f) => { if (!cancelled) setShareFile(f); }) .catch(() => { /* keep local */ }); return () => { cancelled = true; }; }, [done?.fileUrl]); const profileGate = useActivityProfileGate(meta?.eventId, meta?.accessMode, Boolean(meta) && !loading); 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 shareCaption = `Check out my photo from ${meta?.eventName || "the event"}!`; const clearSelectedPhoto = useCallback(() => { setFile(null); setPreviewUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return null; }); }, []); const applySelectedFile = useCallback((next: File) => { setFile(next); setPreviewUrl((prev) => { if (prev) URL.revokeObjectURL(prev); return URL.createObjectURL(next); }); }, []); const cropTarget = useMemo(() => { const slot = selectedFrame?.photoSlot; if (slot?.w && slot?.h) { return { w: Math.max(1, slot.w), h: Math.max(1, slot.h), fromFrame: true as const }; } if (meta?.requireUploadCrop) { return { w: Math.max(1, Number(meta.maxPhotoWidth) || 1080), h: Math.max(1, Number(meta.maxPhotoHeight) || 1080), fromFrame: false as const, }; } return null; }, [selectedFrame, meta?.requireUploadCrop, meta?.maxPhotoWidth, meta?.maxPhotoHeight]); const handleFileChange = async (selected: File | null) => { if (!selected) return; if (!selected.type.startsWith("image/")) { setErr("Please choose an image file"); return; } setErr(null); setPicking(true); try { // Prefer the selected frame's photo-slot aspect (e.g. square slot → crop to 1:1). // Fall back to Settings → require crop + max W/H when no slot is defined. if (!cropTarget) { applySelectedFile(selected); return; } const natural = await getImageNaturalSize(selected); if (ratiosMatch(natural.width, natural.height, cropTarget.w, cropTarget.h)) { 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); } }; const onSubmit = async () => { if (!uploadToken || !file || !meta || !selectedFrame) return; setSubmitting(true); setErr(null); setActionMsg(null); try { const result = await uploadSocialSharePhoto( uploadToken, file, selectedFrame._id, displayName.trim(), profileGate.participantId, ); setShareFile(null); setResultImageReady(false); setDone({ previewUrl: result.previewUrl, fileUrl: result.fileUrl, }); clearSelectedPhoto(); if (result.fileUrl) { void fetchImageFile(result.fileUrl, "social-share.jpg") .then((f) => setShareFile(f)) .catch(() => { void ensureShareableImageFile(file, "social-share.jpg").then(setShareFile); }); } } catch (e2) { setErr(e2 instanceof Error ? e2.message : "Upload failed"); } finally { setSubmitting(false); } }; const resetForNew = () => { setDone(null); setResultImageReady(false); setShareFile(null); setActionMsg(null); setErr(null); setSelectedFrame(null); clearSelectedPhoto(); }; 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 || "social-share.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, "social-share.jpg"); setActionMsg("Download started"); } catch (e) { setActionMsg(e instanceof Error ? e.message : "Download failed"); } }; const onNativeShare = async () => { const src = done?.fileUrl || done?.previewUrl; setActionMsg(null); try { const result = await shareViaNativeSheet({ file: shareFile, imageUrl: src, imgEl: resultImgRef.current, title: meta?.eventName || "Social Share", 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 || "Social Share", 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"); } }; if (!uploadToken) { return (

Missing link

); } if (loading || (meta && !profileGate.allowed)) { return (

Loading…

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

{err}

); } if (done) { return (

Your photo

Here's your framed photo. You can download or share it.

{(done.fileUrl || done.previewUrl) ? (
{!resultImageReady ? (

Preparing your photo…

) : null} Your framed photo setResultImageReady(true)} onError={(e) => { const img = e.currentTarget; 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; return; } setResultImageReady(true); }} />
) : (

Photo is ready, but preview is unavailable.

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

{actionMsg}

: null}
); } return (
{!selectedFrame ? (

Choose a frame

Pick a frame, then upload your photo.

{!meta?.frames?.length ? (

No frames available yet.

) : (
{meta.frames.map((frame) => ( ))}
)} {err ?

{err}

: null}
) : (

Upload photo

Selected frame: {selectedFrame.name}

{previewUrl ? (
Your photo
) : null} void handleFileChange(selected)} /> {err ?

{err}

: null}
)}
{cropSession && cropTarget ? ( { URL.revokeObjectURL(cropSession.imageSrc); setCropSession(null); }} onConfirm={(cropped) => { URL.revokeObjectURL(cropSession.imageSrc); setCropSession(null); applySelectedFile(cropped); }} /> ) : null}
); }