parfum_agsd/parfum-shop/src/pages/DiscoverySetPage.jsx

410 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useLayoutEffect, useRef, useState } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import perfumes from "../data/perfumes";
import SharedNavbar from "../components/SharedNavbar";
import Grid, { Col } from "../components/layout/Grid";
import PageMeta from "../components/seo/PageMeta";
import StickyBuyBar from "../components/StickyBuyBar";
import { useShop } from "../shop/useShop";
import "./DiscoverySetPage.css";
gsap.registerPlugin(ScrollTrigger);
const DISCOVERY_SET_IMAGE = "/atmos-discovery-set-thumbnail.webp";
const discoveryPanelFacts = [
{ label: "Umfang", value: "6 × 2ml" },
{ label: "Gutschrift", value: "CHF 48 werden beim späteren 50-ml-Kauf berücksichtigt." },
];
const discoveryBenefits = [
{
title: "6 × 2-ml-Proben aller Signature-Düfte",
text: "Kalter Beton, Schwarzes Benzin, Verbranntes Chrom, Blasse Seide, Weisse Asche und Nasser Marmor.",
},
{
title: "CHF 48 Gutschein automatisch im Set",
text: "Nur das erste Discovery Set erstellt den einmaligen Rabatt. Er wird bei einem späteren 50-ml-Kauf automatisch angerechnet.",
},
{
title: "Jede Probe = ca. 20 Anwendungen",
text: "Genug, um jeden Duft mehrere Tage im Alltag und in unterschiedlichen Situationen zu testen.",
},
{
title: "Hochwertige Mini-Flacons",
text: "Sorgfältig zusammengestellt, reduziert gestaltet und als Teil des atmos Konzepts gedacht.",
},
];
const discoverySteps = [
{
number: "01",
title: "Bestellen",
text: "Discovery Set für CHF 48 bestellen. Nur dein erstes Set erzeugt automatisch einen einmaligen Rabatt.",
},
{
number: "02",
title: "Testen",
text: "Jeden Duft mindestens einige Tage tragen. Im Alltag, zu verschiedenen Anlässen und auf der eigenen Haut.",
},
{
number: "03",
title: "Entscheiden",
text: "50-ml-Flakon bestellen. CHF 48 werden automatisch angerechnet, sofern der Rabatt noch nicht genutzt wurde.",
},
];
function DiscoveryOrderPanel({ onBuy, panelRef }) {
return (
<aside className="discovery-order-panel" ref={panelRef}>
<div className="discovery-price-row">
<span>Preis</span>
<strong>CHF 48.</strong>
</div>
<div className="discovery-panel-facts">
{discoveryPanelFacts.map((item) => (
<div key={item.label}>
<span>{item.label}</span>
<p>{item.value}</p>
</div>
))}
</div>
<div className="discovery-panel-actions">
<button
type="button"
className="atmos-btn atmos-btn--primary atmos-btn--block atmos-btn--uppercase"
onClick={onBuy}
>
Kaufen
</button>
<p>Nur das erste Set erstellt einen einmaligen CHF 48 Rabatt auf den 50-ml-Kauf.</p>
</div>
</aside>
);
}
function DiscoveryHero({ onBuy, panelRef }) {
return (
<section className="discovery-hero" data-reveal-group data-reveal-start="top 90%">
<div className="discovery-hero-stage">
<div className="discovery-hero-copy" data-reveal="fade">
<span className="discovery-kicker">Discovery Set</span>
<h1>Der Einstieg</h1>
<p className="discovery-intro">
6 Düfte × 2ml. Jeden Duft eine Woche tragen. Verstehen, was
wirklich funktioniert. Ohne Risiko. Der sichere Einstieg in die
Welt der Nischendüfte, bevor du dich für einen 50-ml-Flakon entscheidest.
</p>
</div>
<figure className="discovery-hero-visual" data-reveal="fade">
<img
src={DISCOVERY_SET_IMAGE}
alt="Atmos Discovery Set"
loading="eager"
decoding="async"
/>
</figure>
</div>
<DiscoveryOrderPanel onBuy={onBuy} panelRef={panelRef} />
</section>
);
}
function DiscoveryStorySection() {
return (
<section className="discovery-story-grid" data-reveal-group>
<div className="discovery-story-copy">
<span className="discovery-label" data-reveal="fade">
Warum Discovery Set
</span>
<h2 data-reveal="lines">Der klügere Einstieg.</h2>
<p data-reveal="fade">
Nischen-Parfums sind keine Impulskäufe. Sie brauchen Zeit, um zu
verstehen, wie sie auf deiner Haut funktionieren, wie sie sich im
Alltag entwickeln und ob sie wirklich zu dir passen.
</p>
</div>
<div className="discovery-benefit-panel" data-reveal="fade">
<div className="discovery-benefit-panel-head">
<span>Testlogik</span>
<strong>6 × 2ml</strong>
</div>
{discoveryBenefits.map((benefit) => (
<article className="discovery-benefit" key={benefit.title}>
<div>
<strong>{benefit.title}</strong>
<p>{benefit.text}</p>
</div>
</article>
))}
</div>
</section>
);
}
function DiscoveryProcessSection() {
return (
<section className="discovery-process-section" data-reveal-group>
<div className="discovery-section-intro">
<span className="discovery-label" data-reveal="fade">
Ablauf
</span>
<h2 data-reveal="lines">So funktioniert&apos;s</h2>
</div>
<div className="discovery-steps">
{discoverySteps.map((step) => (
<article key={step.number} className="discovery-step" data-reveal="fade">
<span className="discovery-step__number">{step.number}</span>
<h3 className="discovery-step__title">{step.title}</h3>
<p className="discovery-step__text">{step.text}</p>
</article>
))}
</div>
</section>
);
}
function DiscoveryIncludedSection() {
const sectionRef = useRef(null);
const itemRefs = useRef([]);
const activeIndexRef = useRef(0);
const [activeIndex, setActiveIndex] = useState(0);
const setItemRef = useCallback((element, index) => {
itemRefs.current[index] = element;
}, []);
const activateItem = useCallback((index) => {
if (activeIndexRef.current === index) {
return;
}
activeIndexRef.current = index;
setActiveIndex(index);
const items = itemRefs.current.filter(Boolean);
gsap.killTweensOf(items);
gsap.to(items, {
opacity: (itemIndex) => (itemIndex === index ? 1 : 0.64),
duration: 0.36,
ease: "power3.out",
overwrite: "auto",
});
}, []);
useLayoutEffect(() => {
const section = sectionRef.current;
const items = itemRefs.current.filter(Boolean);
if (!section || items.length === 0 || typeof window === "undefined") {
return undefined;
}
const prefersReducedMotion = window.matchMedia(
"(prefers-reduced-motion: reduce)"
).matches;
if (prefersReducedMotion) {
activeIndexRef.current = 0;
return undefined;
}
const ctx = gsap.context(() => {
gsap.set(items, {
opacity: (index) => (index === activeIndexRef.current ? 1 : 0.64),
force3D: true,
});
const updateActiveItem = () => {
const viewportCenter = window.innerHeight / 2;
let closestIndex = 0;
let closestDistance = Number.POSITIVE_INFINITY;
items.forEach((item, index) => {
const rect = item.getBoundingClientRect();
const itemCenter = rect.top + rect.height / 2;
const distance = Math.abs(itemCenter - viewportCenter);
if (distance < closestDistance) {
closestDistance = distance;
closestIndex = index;
}
});
activateItem(closestIndex);
};
ScrollTrigger.create({
trigger: section,
start: "top bottom",
end: "bottom top",
onEnter: updateActiveItem,
onEnterBack: updateActiveItem,
onUpdate: updateActiveItem,
onRefresh: updateActiveItem,
});
updateActiveItem();
ScrollTrigger.refresh();
}, section);
return () => {
gsap.killTweensOf(items);
ctx.revert();
};
}, [activateItem]);
return (
<section className="discovery-included" data-reveal-group ref={sectionRef}>
<div className="discovery-section-heading">
<span className="discovery-label" data-reveal="fade">
Im Set enthalten
</span>
<h2 className="discovery-included-title" data-reveal="lines">
6 Signature
<br />
Düfte
</h2>
</div>
<div className="discovery-included-layout">
<ol className="discovery-scroll-list" aria-label="Düfte im Discovery Set">
{perfumes.map((perfume, index) => {
const isActive = activeIndex === index;
return (
<li className="discovery-scroll-list-item" key={perfume.id} data-reveal-group data-reveal-start="top 84%">
<article
className={`discovery-scroll-item${
isActive ? " discovery-scroll-item--active" : ""
}`}
aria-current={isActive ? "true" : undefined}
ref={(element) => setItemRef(element, index)}
>
<span className="discovery-product-index" data-reveal="fade">{perfume.id}</span>
<div className="discovery-scroll-copy" data-reveal="fade">
<h3>{perfume.name}</h3>
<div className="discovery-scroll-line" aria-hidden="true" />
<div className="discovery-scroll-mobile-detail">
<p>{perfume.text}</p>
<p>{perfume.mood}</p>
</div>
<div className="discovery-product-tags discovery-scroll-mobile-tags">
{perfume.materialTags.slice(0, 3).map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
</div>
</article>
</li>
);
})}
</ol>
<aside className="discovery-story-visual-panel" aria-label="Aktiver Duft">
<div className="discovery-story-visual-images" aria-hidden="true">
{perfumes.map((perfume, index) => {
const moodImage =
perfume.storyVisualImage || perfume.phaseVisualImages?.[0];
return (
<img
key={perfume.id}
src={moodImage}
alt=""
className={activeIndex === index ? "is-active" : ""}
loading="lazy"
decoding="async"
/>
);
})}
</div>
<div className="discovery-story-visual-content">
<span>Material-Komposition</span>
<div className="discovery-story-tags">
{perfumes[activeIndex].materialTags.map((tag) => (
<span key={tag}>{tag}</span>
))}
</div>
</div>
</aside>
</div>
</section>
);
}
function DiscoveryFinalCta({ onBuy }) {
return (
<section className="discovery-final-cta" data-reveal-group data-on-accent>
<div>
<span className="discovery-label" data-reveal="fade">
Discovery Set
</span>
<h2 data-reveal="lines">Der sichere Einstieg.</h2>
<p data-reveal="fade">
Kostenloser Versand · 23 Werktage · Einmalige Anrechnung beim 50-ml-Kauf
</p>
</div>
<div className="discovery-final-actions" data-reveal="fade">
<button
type="button"
className="atmos-btn atmos-btn--primary atmos-btn--uppercase"
onClick={onBuy}
>
Discovery Set bestellen
</button>
</div>
</section>
);
}
function DiscoverySetPage() {
const { addToCart } = useShop();
const orderPanelRef = useRef(null);
const buyDiscoverySet = () =>
addToCart("discovery-set", 1, "Discovery Set wurde in den Warenkorb gelegt.").catch(() => {});
return (
<div className="discovery-page">
<PageMeta
title="Discovery Set"
description="6 × 2-ml-Proben aller atmos-Düfte. Eine Woche pro Duft tragen, verstehen, welcher passt. CHF 48, anrechenbar beim 50-ml-Kauf."
path="/discovery-set"
/>
<SharedNavbar variant="hero" active="testen" />
<main id="main-content" className="shell">
<DiscoveryHero onBuy={buyDiscoverySet} panelRef={orderPanelRef} />
<DiscoveryStorySection />
<DiscoveryIncludedSection />
<DiscoveryProcessSection />
<DiscoveryFinalCta onBuy={buyDiscoverySet} />
</main>
<StickyBuyBar
label="Discovery Set"
price="CHF 48."
observeRef={orderPanelRef}
onBuy={buyDiscoverySet}
alwaysVisibleMobile
/>
</div>
);
}
export default DiscoverySetPage;