157 lines
5.1 KiB
TypeScript
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>
|
|
);
|
|
}
|