114 lines
3.8 KiB
TypeScript
114 lines
3.8 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { useParams } from "react-router-dom";
|
|
import { getPhotoWallPresent, type PhotoWallPresentData } from "../lib/api";
|
|
|
|
const POLL_MS = 4000;
|
|
|
|
export function PhotoWallPresentPage() {
|
|
const { presentToken } = useParams<{ presentToken: string }>();
|
|
const [data, setData] = useState<PhotoWallPresentData | null>(null);
|
|
const [err, setErr] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const refresh = useCallback(async () => {
|
|
if (!presentToken) return;
|
|
try {
|
|
const next = await getPhotoWallPresent(presentToken);
|
|
setData(next);
|
|
setErr(null);
|
|
} catch (e) {
|
|
setErr(e instanceof Error ? e.message : "Photo wall not available");
|
|
setData(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [presentToken]);
|
|
|
|
useEffect(() => {
|
|
void refresh();
|
|
const id = window.setInterval(() => void refresh(), POLL_MS);
|
|
return () => window.clearInterval(id);
|
|
}, [refresh]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="eb-gradient-auth flex min-h-dvh items-center justify-center text-white/80">
|
|
Loading wall…
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!data) {
|
|
return (
|
|
<div className="eb-gradient-auth flex min-h-dvh flex-col items-center justify-center gap-2 px-6 text-center text-white">
|
|
<p className="text-lg font-semibold">Photo wall unavailable</p>
|
|
<p className="text-sm text-white/80">{err}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const cols = Math.max(1, Math.min(12, data.photosPerRow || 4));
|
|
const maxW = data.maxPhotoWidth || 320;
|
|
const maxH = data.maxPhotoHeight || 320;
|
|
const gap = data.gapPx ?? 8;
|
|
const showNames = Boolean(data.showUploaderNameOnWall);
|
|
const frameEnabled = Boolean(data.frameEnabled);
|
|
const frameW = data.frameCanvasWidth || 1080;
|
|
const frameH = data.frameCanvasHeight || 1080;
|
|
const aspectRatio = frameEnabled ? `${frameW} / ${frameH}` : `${maxW} / ${maxH}`;
|
|
|
|
return (
|
|
<div className="eb-gradient-auth min-h-dvh text-white">
|
|
<div className="px-4 pb-3 pt-4 text-center card-bg rounded-lg shadow-sm max-w-[400px] mx-auto mb-3">
|
|
<p className="text-xs uppercase tracking-[0.2em] text-[var(--theme-card-text-color)]/70">Photo Wall</p>
|
|
<h1 className="text-xl font-semibold sm:text-2xl text-[var(--theme-card-text-color)]">{data.eventName}</h1>
|
|
</div>
|
|
|
|
{data.photos.length === 0 ? (
|
|
<div className="flex min-h-[60dvh] items-center justify-center px-6 text-center text-white/80">
|
|
Waiting for photos…
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="mx-auto w-full max-w-[100vw] px-3 pb-6"
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))`,
|
|
gap: `${gap}px`,
|
|
justifyItems: "center",
|
|
alignItems: "start",
|
|
}}
|
|
>
|
|
{data.photos.map((p) => (
|
|
<div
|
|
key={p._id}
|
|
className="flex w-full flex-col items-center"
|
|
style={{ maxWidth: maxW }}
|
|
>
|
|
<img
|
|
src={p.url}
|
|
alt=""
|
|
className="w-full rounded-md object-cover shadow-lg"
|
|
style={{
|
|
maxWidth: maxW,
|
|
maxHeight: maxH,
|
|
aspectRatio,
|
|
objectFit: "cover",
|
|
}}
|
|
/>
|
|
{showNames && p.displayName ? (
|
|
<p
|
|
className="mt- w-full truncate text-center text-xs font-medium text-[var(--theme-card-text-color)]/90 bg-[var(--theme-card-color)] rounded-md p-1 sm:text-sm"
|
|
title={p.displayName}
|
|
>
|
|
{p.displayName}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|