EventBuzz_Website/src/components/photoWall/PhotoSpotlight.tsx

157 lines
5.1 KiB
TypeScript

import { useEffect, useRef } from "react";
import gsap from "gsap";
import type { PresentPhoto } from "../../lib/photoWallMarquee";
type PhotoSpotlightProps = {
photo: PresentPhoto | null;
borderRadius: number | string;
holdMs?: number;
onComplete: () => void;
};
/**
* Full-screen spotlight for a newly added photo: fade/scale in → hold → out.
* Optional `message` is shown as a quote (spotlight only — not on wall cards).
*/
export function PhotoSpotlight({
photo,
borderRadius,
holdMs = 2400,
onComplete,
}: PhotoSpotlightProps) {
const rootRef = useRef<HTMLDivElement>(null);
const cardRef = useRef<HTMLDivElement>(null);
const frameRef = useRef<HTMLDivElement>(null);
const nameRef = useRef<HTMLParagraphElement>(null);
const messageRef = useRef<HTMLQuoteElement>(null);
const onCompleteRef = useRef(onComplete);
onCompleteRef.current = onComplete;
const message = photo?.message?.trim() || "";
const effectiveHoldMs = message ? Math.max(holdMs, 3200) : holdMs;
useEffect(() => {
if (!photo || !rootRef.current || !cardRef.current || !frameRef.current) return;
const ctx = gsap.context(() => {
const tl = gsap.timeline({
onComplete: () => onCompleteRef.current(),
});
gsap.set(rootRef.current, { autoAlpha: 0 });
gsap.set(cardRef.current, { scale: 0.82, autoAlpha: 0 });
gsap.set(frameRef.current, {
boxShadow:
"0 0 0 0 color-mix(in srgb, var(--theme-button-color) 0%, transparent)",
});
if (nameRef.current) {
gsap.set(nameRef.current, { autoAlpha: 0, y: 18 });
}
if (messageRef.current) {
gsap.set(messageRef.current, { autoAlpha: 0, y: 22 });
}
tl.to(rootRef.current, { autoAlpha: 1, duration: 0.35, ease: "power2.out" })
.to(
cardRef.current,
{ scale: 1, autoAlpha: 1, duration: 0.45, ease: "back.out(1.4)" },
"<0.05",
)
.to(
frameRef.current,
{
boxShadow:
"0 0 48px 18px color-mix(in srgb, var(--theme-button-color) 22%, transparent), 0 0 90px 36px color-mix(in srgb, var(--theme-button-color) 12%, transparent), 0 16px 36px rgba(0,0,0,0.35)",
duration: 0.55,
ease: "power2.out",
},
"<0.1",
);
if (nameRef.current) {
tl.to(
nameRef.current,
{ autoAlpha: 1, y: 0, duration: 0.45, ease: "power3.out" },
"-=0.15",
);
}
if (messageRef.current) {
tl.to(
messageRef.current,
{ autoAlpha: 1, y: 0, duration: 0.5, ease: "power3.out" },
"-=0.2",
);
}
tl.to({}, { duration: effectiveHoldMs / 1000 })
.to(cardRef.current, { scale: 0.9, autoAlpha: 0, duration: 0.35, ease: "power2.in" })
.to(rootRef.current, { autoAlpha: 0, duration: 0.25, ease: "power2.in" }, "<");
});
return () => ctx.revert();
}, [photo, effectiveHoldMs]);
if (!photo) return null;
return (
<div
ref={rootRef}
className="pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black/55 px-6 backdrop-blur-[2px]"
role="dialog"
aria-label="New photo"
>
<div
ref={cardRef}
className="pointer-events-auto flex w-full max-w-[min(440px,90vw)] flex-col items-center"
>
<div
ref={frameRef}
className="w-full overflow-hidden ring-2 ring-white/35"
style={{ borderRadius, aspectRatio: "1 / 1" }}
>
<img
src={photo.url}
alt={photo.displayName || "New photo"}
className="h-full w-full object-cover"
/>
</div>
<div className="mt-5 flex w-full flex-col items-center px-1 text-center">
{photo.displayName ? (
<p
ref={nameRef}
className="text-[2.1rem] font-bold leading-tight tracking-tight text-[var(--theme-card-text-color)] drop-shadow-md sm:text-[2.45rem]"
style={{ fontFamily: "var(--theme-heading-font, inherit)" }}
>
{photo.displayName}
</p>
) : null}
{message ? (
<blockquote ref={messageRef} className="relative mt-5 max-w-[24rem] px-6">
<span
aria-hidden
className="pointer-events-none absolute -left-1 -top-4 select-none text-6xl font-serif leading-none text-[var(--theme-button-color)]/40"
>
“
</span>
<p
className="text-[1.15rem] italic leading-relaxed text-[var(--theme-card-text-color)]/90 sm:text-[1.3rem]"
style={{ fontFamily: "var(--theme-body-font, Georgia, 'Times New Roman', serif)" }}
>
{message}
</p>
<span
aria-hidden
className="pointer-events-none absolute -bottom-6 right-0 select-none text-6xl font-serif leading-none text-[var(--theme-button-color)]/40"
>
”
</span>
</blockquote>
) : null}
</div>
</div>
</div>
);
}