278 lines
7.4 KiB
JavaScript
278 lines
7.4 KiB
JavaScript
import { useEffect, useRef } from "react";
|
|
import { Link } from "react-router";
|
|
import { gsap } from "gsap";
|
|
import perfumes from "../data/perfumes";
|
|
import "../pages/LandingPage.css";
|
|
import "../style/navbar.css";
|
|
|
|
function LandingPage() {
|
|
const cardRefs = useRef([]);
|
|
|
|
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 (
|
|
<div className="page">
|
|
<header className="hero">
|
|
<nav className="navbar navbar--hero">
|
|
<div className="nav-pill">
|
|
<a href="#home" className="nav-link active">
|
|
Name
|
|
</a>
|
|
<a href="#dufte" className="nav-link">
|
|
Düfte
|
|
</a>
|
|
<a href="#testen" className="nav-link">
|
|
Testen
|
|
</a>
|
|
<a href="#cart" className="nav-link">
|
|
Cart
|
|
</a>
|
|
</div>
|
|
</nav>
|
|
|
|
<div className="hero-overlay" />
|
|
|
|
<div className="hero-content">
|
|
<p className="eyebrow">NISCHENDÜFTE</p>
|
|
<h1>
|
|
DÜFTE ALS
|
|
<br />
|
|
AUSDRUCK
|
|
<br />
|
|
VON KONZEPT
|
|
</h1>
|
|
<p className="hero-text">
|
|
Konzeptuelle Düfte zwischen Materialität, Raum und Charakter.
|
|
</p>
|
|
|
|
<div className="hero-actions">
|
|
<button className="btn btn-primary">Aktuelle Düfte</button>
|
|
<button className="btn btn-secondary">Discovery Set</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main>
|
|
<section className="section" id="dufte">
|
|
<div className="section-heading">
|
|
<h2>
|
|
WÄHLE EINE
|
|
<br />
|
|
ATMOSPHÄRE
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="product-grid">
|
|
{perfumes.map((item, index) => (
|
|
<Link
|
|
to={`/duft/${item.slug}`}
|
|
className="product-card"
|
|
key={item.id}
|
|
ref={(el) => {
|
|
cardRefs.current[index] = el;
|
|
}}
|
|
>
|
|
<div className="product-hover-fill" aria-hidden="true">
|
|
{item.fillVideo ? (
|
|
<video
|
|
className="product-hover-video"
|
|
src={item.fillVideo}
|
|
muted
|
|
loop
|
|
playsInline
|
|
preload="metadata"
|
|
/>
|
|
) : (
|
|
<div
|
|
className="product-hover-image"
|
|
style={{
|
|
backgroundImage: `url(${item.fillImage || item.image})`,
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<div className="product-top">
|
|
<span className="product-id">{item.id}</span>
|
|
<h3>{item.name}</h3>
|
|
</div>
|
|
|
|
<div className="product-image-wrap">
|
|
<img src={item.image} alt={item.name} className="product-image" />
|
|
</div>
|
|
|
|
<div className="product-bottom">
|
|
<p>{item.text}</p>
|
|
<span className="arrow">→</span>
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="discovery-section" id="testen">
|
|
<div className="discovery-copy">
|
|
<h2>
|
|
DER SICHERE EINSTIEG
|
|
<br />
|
|
DISCOVERY SET
|
|
</h2>
|
|
<p>
|
|
Alle 6 Düfte als 2ml Samples 2ml.
|
|
<br />
|
|
Jeden Duft eine Woche tragen.
|
|
<br />
|
|
Verstehen, was funktioniert.
|
|
</p>
|
|
<button className="discovery-btn">Discovery Set bestellen</button>
|
|
</div>
|
|
|
|
<div className="discovery-banner">
|
|
<img src="/DISCOVERYSET.png" alt="Discovery Set" />
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default LandingPage; |