Files
blackheart-website/src/components/ui/Reveal.tsx
T
diana.dolgolyova a587736dd3 feat: mobile UX, admin polish, rate limiting, and media assets
- Mobile responsiveness improvements across admin and public sections
- Admin: bookings modal, open-day page, team page, layout polish
- Added rate limiting, CSRF hardening, auth-edge improvements
- Scroll reveal, floating contact, back-to-top, Yandex map fixes
- Schedule filters refactor, team profile/info component updates
- New useTrainerPhotos hook
- Added class, team, master-class, and news images
2026-04-10 18:42:54 +03:00

74 lines
1.8 KiB
TypeScript

"use client";
import { useEffect, useRef, useState } from "react";
/** Shared singleton IntersectionObserver for all Reveal instances */
const callbacks = new Map<Element, () => void>();
let sharedObserver: IntersectionObserver | null = null;
function getObserver(): IntersectionObserver {
if (!sharedObserver) {
sharedObserver = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const cb = callbacks.get(entry.target);
if (cb) {
cb();
callbacks.delete(entry.target);
sharedObserver!.unobserve(entry.target);
}
}
}
},
{ threshold: 0.1, rootMargin: "0px 0px -50px 0px" },
);
}
return sharedObserver;
}
interface RevealProps {
children: React.ReactNode;
className?: string;
}
export function Reveal({ children, className = "" }: RevealProps) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
const [reducedMotion, setReducedMotion] = useState(false);
useEffect(() => {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setReducedMotion(true);
setVisible(true);
return;
}
const el = ref.current;
if (!el) return;
const observer = getObserver();
callbacks.set(el, () => setVisible(true));
observer.observe(el);
return () => {
callbacks.delete(el);
observer.unobserve(el);
};
}, []);
return (
<div
ref={ref}
className={className}
style={reducedMotion ? undefined : {
opacity: visible ? 1 : 0,
transform: visible ? "translateY(0)" : "translateY(30px)",
transition: "opacity 0.7s ease-out, transform 0.7s ease-out",
}}
>
{children}
</div>
);
}