import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { Link } from "react-router";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import HeroSection from "../components/landing/HeroSection";
import SharedNavbar from "../components/SharedNavbar";
import { useProductTransition } from "../transitions/ProductTransitionContext";
import perfumes from "../data/perfumes";
import "../pages/LandingPage.css";
import "../style/navbar.css";
const INTRO_SESSION_KEY = "atmos-landing-intro-played";
gsap.registerPlugin(ScrollTrigger);
function LandingPage() {
const pageRef = useRef(null);
const overlayRef = useRef(null);
const overlayTextRef = useRef(null);
const heroImageWrapRef = useRef(null);
const heroImageRef = useRef(null);
const discoveryImageRef = useRef(null);
const headlineLineRefs = useRef([]);
const heroMetaRefs = useRef([]);
const cardRefs = useRef([]);
const { startProductTransition } = useProductTransition();
const [introSettings] = useState(() => {
if (typeof window === "undefined") {
return { shouldPlayIntro: true };
}
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
const introAlreadyPlayed =
window.sessionStorage.getItem(INTRO_SESSION_KEY) === "true";
return {
shouldPlayIntro: !prefersReducedMotion && !introAlreadyPlayed,
};
});
const { shouldPlayIntro } = introSettings;
const setHeadlinePrimaryRef = useCallback((element) => {
headlineLineRefs.current[0] = element;
}, []);
const setHeadlineSecondaryRef = useCallback((element) => {
headlineLineRefs.current[1] = element;
}, []);
const setDescriptionRef = useCallback((element) => {
heroMetaRefs.current[0] = element;
}, []);
const setActionsRef = useCallback((element) => {
heroMetaRefs.current[1] = element;
}, []);
useLayoutEffect(() => {
const overlay = overlayRef.current;
const overlayText = overlayTextRef.current;
const heroImageWrap = heroImageWrapRef.current;
const headlineLines = headlineLineRefs.current.filter(Boolean);
const heroMeta = heroMetaRefs.current.filter(Boolean);
if (!overlay || !overlayText || !heroImageWrap || headlineLines.length === 0) {
return undefined;
}
const ctx = gsap.context(() => {
gsap.set(heroImageWrap, {
scale: 1.22,
transformOrigin: "center center",
});
gsap.set(headlineLines, {
yPercent: 115,
rotate: 2.2,
transformOrigin: "0% 100%",
force3D: true,
});
gsap.set(heroMeta, {
y: 36,
autoAlpha: 0,
});
if (!shouldPlayIntro) {
gsap.set(heroImageWrap, { scale: 1 });
gsap.set(headlineLines, { yPercent: 0, rotate: 0 });
gsap.set(heroMeta, { y: 0, autoAlpha: 1 });
gsap.set(overlay, {
yPercent: -100,
autoAlpha: 0,
display: "none",
});
return;
}
gsap.set(overlay, { yPercent: 0, autoAlpha: 1, display: "block" });
gsap.set(overlayText, { xPercent: 28, yPercent: 140, autoAlpha: 0 });
const introTimeline = gsap.timeline({
defaults: { ease: "power3.out" },
onComplete: () => {
window.sessionStorage.setItem(INTRO_SESSION_KEY, "true");
},
});
introTimeline
.to(overlayText, {
xPercent: 0,
yPercent: 0,
autoAlpha: 1,
duration: 1.28,
ease: "expo.out",
})
.to({}, { duration: 0.34 })
.to(
overlay,
{
yPercent: -100,
duration: 1.46,
ease: "power4.out",
},
">"
)
.to(
heroImageWrap,
{
scale: 1,
duration: 1.6,
ease: "power2.out",
},
"<0.03"
)
.to(
headlineLines,
{
yPercent: 0,
rotate: 0,
duration: 1.18,
stagger: 0.1,
ease: "power4.out",
},
"<0.27"
)
.to(
heroMeta,
{
y: 0,
autoAlpha: 1,
duration: 1.02,
stagger: 0.12,
ease: "power4.out",
},
"<0.16"
)
.set(overlay, { display: "none" });
}, pageRef);
return () => {
ctx.revert();
};
}, [shouldPlayIntro]);
useLayoutEffect(() => {
const heroImageWrap = heroImageWrapRef.current;
const heroImage = heroImageRef.current;
const discoveryImage = discoveryImageRef.current;
if (!heroImageWrap || !heroImage || !discoveryImage || typeof window === "undefined") {
return undefined;
}
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
return undefined;
}
const heroSection = heroImageWrap.closest(".hero");
const discoveryBanner = discoveryImage.closest(".discovery-banner");
if (!heroSection || !discoveryBanner) {
return undefined;
}
const ctx = gsap.context(() => {
gsap.set(heroImage, {
scale: 1.14,
yPercent: -4,
transformOrigin: "center center",
force3D: true,
});
gsap.to(heroImage, {
yPercent: 8,
ease: "none",
scrollTrigger: {
trigger: heroSection,
start: "top top",
end: "bottom top",
scrub: 1.15,
},
});
gsap.set(discoveryImage, {
scale: 1.16,
yPercent: -8,
transformOrigin: "center center",
force3D: true,
});
gsap.to(discoveryImage, {
yPercent: 10,
ease: "none",
scrollTrigger: {
trigger: discoveryBanner,
start: "top bottom",
end: "bottom top",
scrub: 1.2,
},
});
}, pageRef);
return () => {
ctx.revert();
};
}, []);
useEffect(() => {
const cards = cardRefs.current.filter(Boolean);
const cardStates = cards
.map((card) => {
const hoverFill = card.querySelector(".product-hover-fill");
const hoverVideo = card.querySelector(".product-hover-video");
const productImage = card.querySelector(".product-image");
const arrow = card.querySelector(".arrow");
if (!hoverFill || !productImage || !arrow) {
return null;
}
gsap.set(hoverFill, { autoAlpha: 0, scale: 1.08 });
gsap.set(productImage, { autoAlpha: 1, scale: 1 });
gsap.set(arrow, { x: 0 });
return { card, hoverFill, hoverVideo, productImage, arrow };
})
.filter(Boolean);
const stopVideo = (video) => {
if (!video) return;
video.pause();
try {
video.currentTime = 0;
} catch {
// Ignore errors when setting currentTime.
}
};
const playVideo = (video) => {
if (!video) return;
try {
video.currentTime = 0;
} catch {
// Ignore errors when setting currentTime.
}
const playAttempt = video.play();
if (playAttempt && typeof playAttempt.catch === "function") {
playAttempt.catch(() => {});
}
};
const deactivate = (state) => {
gsap.killTweensOf([state.hoverFill, state.productImage, state.arrow]);
stopVideo(state.hoverVideo);
gsap.to(state.hoverFill, {
autoAlpha: 0,
scale: 1.08,
duration: 0.35,
ease: "power2.out",
overwrite: "auto",
});
gsap.to(state.productImage, {
scale: 1,
autoAlpha: 1,
duration: 0.35,
ease: "power2.out",
overwrite: "auto",
});
gsap.to(state.arrow, {
x: 0,
duration: 0.35,
ease: "power2.out",
overwrite: "auto",
});
};
const activate = (state) => {
cardStates.forEach((item) => {
if (item !== state) {
deactivate(item);
}
});
gsap.killTweensOf([state.hoverFill, state.productImage, state.arrow]);
playVideo(state.hoverVideo);
gsap.to(state.hoverFill, {
autoAlpha: 1,
scale: 1,
duration: 0.45,
ease: "power2.out",
overwrite: "auto",
});
gsap.to(state.productImage, {
scale: 0.92,
autoAlpha: 0.35,
duration: 0.45,
ease: "power2.out",
overwrite: "auto",
});
gsap.to(state.arrow, {
x: 8,
duration: 0.45,
ease: "power2.out",
overwrite: "auto",
});
};
const cardCleanups = cardStates.map((state) => {
const onEnter = () => activate(state);
const onLeave = () => deactivate(state);
state.card.addEventListener("pointerenter", onEnter);
state.card.addEventListener("pointerleave", onLeave);
return () => {
state.card.removeEventListener("pointerenter", onEnter);
state.card.removeEventListener("pointerleave", onLeave);
};
});
const resetAll = () => {
cardStates.forEach((state) => deactivate(state));
};
const onMouseOutWindow = (event) => {
if (!event.relatedTarget) {
resetAll();
}
};
window.addEventListener("blur", resetAll);
document.addEventListener("mouseout", onMouseOutWindow);
return () => {
window.removeEventListener("blur", resetAll);
document.removeEventListener("mouseout", onMouseOutWindow);
cardCleanups.forEach((cleanup) => cleanup());
};
}, []);
return (
{"W\u00C4HLE EINE"}
{"ATMOSPH\u00C4RE"}
{perfumes.map((item, index) => (
startProductTransition(event, item)}
ref={(element) => {
cardRefs.current[index] = element;
}}
>
{item.fillVideo ? (
) : (
)}
{item.id}
{item.name}
))}
DER SICHERE EINSTIEG
DISCOVERY SET
{"Alle 6 D\u00FCfte als 2ml Samples."}
Jeden Duft eine Woche tragen.
Verstehen, was funktioniert.
Discovery Set bestellen
);
}
export default LandingPage;