diff --git a/parfum-shop/public/atmos-hero-image.png b/parfum-shop/public/atmos-hero-image.png new file mode 100644 index 0000000..a0a4062 Binary files /dev/null and b/parfum-shop/public/atmos-hero-image.png differ diff --git a/parfum-shop/public/atmos-logo-dark.svg b/parfum-shop/public/atmos-logo-dark.svg new file mode 100644 index 0000000..45fcc3d --- /dev/null +++ b/parfum-shop/public/atmos-logo-dark.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/parfum-shop/public/atmos-logo-light.svg b/parfum-shop/public/atmos-logo-light.svg new file mode 100644 index 0000000..9cb8d6b --- /dev/null +++ b/parfum-shop/public/atmos-logo-light.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/parfum-shop/src/components/landing/HeroSection.jsx b/parfum-shop/src/components/landing/HeroSection.jsx new file mode 100644 index 0000000..226f507 --- /dev/null +++ b/parfum-shop/src/components/landing/HeroSection.jsx @@ -0,0 +1,87 @@ +import { Link } from "react-router"; +import IntroOverlay from "./IntroOverlay"; + +function HeroSection({ + heroImageWrapRef, + setHeadlinePrimaryRef, + setHeadlineSecondaryRef, + setDescriptionRef, + setActionsRef, + overlayRef, + overlayTextRef, +}) { + return ( +
+
+ Atmos Hero +
+ + + atmos + + + + +
+

+ + {"D\u00DCFTE ALS"} + + + AUSDRUCK + +

+ +

+ { + "Konzeptionelle D\u00FCfte zwischen Materialit\u00E4t, Raum und Charakter." + } +

+ +
+ + {"Aktuelle D\u00FCfte"} + + + Discovery Set + +
+
+ + +
+ ); +} + +export default HeroSection; diff --git a/parfum-shop/src/components/landing/IntroOverlay.jsx b/parfum-shop/src/components/landing/IntroOverlay.jsx new file mode 100644 index 0000000..7c55e28 --- /dev/null +++ b/parfum-shop/src/components/landing/IntroOverlay.jsx @@ -0,0 +1,15 @@ +function IntroOverlay({ overlayRef, overlayTextRef, logoSrc }) { + return ( + + ); +} + +export default IntroOverlay; diff --git a/parfum-shop/src/pages/LandingPage.css b/parfum-shop/src/pages/LandingPage.css index cfd4261..3ca41e0 100644 --- a/parfum-shop/src/pages/LandingPage.css +++ b/parfum-shop/src/pages/LandingPage.css @@ -4,78 +4,135 @@ color: #1f1f1f; } +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + /* HERO */ .hero { position: relative; - min-height: 720px; - margin-left: 20px; - margin-right: 20px; - margin-top: 0px; - border-radius: 0; + width: 100%; + min-height: 100vh; + min-height: 100svh; + min-height: 100dvh; overflow: hidden; - background-image: url("/HERO.jpeg"); - background-size: cover; - background-position: center; + display: flex; + align-items: center; + isolation: isolate; + background: #111; } -.hero-overlay { +.hero-media { position: absolute; inset: 0; - background: - linear-gradient(to right, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.1)), - linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.45)); + z-index: 1; + will-change: transform; +} + +.hero-media__image { + width: 100%; + height: 100%; + display: block; + object-fit: cover; + object-position: center; +} + +.hero .navbar--hero { + position: absolute; + top: 22px; + left: 0; + right: 0; + z-index: 12; + padding-top: 0; +} + +.hero-brand { + position: absolute; + top: 22px; + left: clamp(1rem, 1.45vw, 20px); + z-index: 14; + display: inline-flex; + align-items: center; +} + +.hero-brand__logo { + display: block; + width: clamp(74px, 8vw, 112px); + height: auto; } .hero-content { position: relative; - z-index: 2; - max-width: 460px; - padding: 120px 0 0 38px; - color: white; + z-index: 6; + width: min(760px, 100%); + padding: clamp(6rem, 11vh, 9rem) clamp(1.2rem, 3.4vw, 3rem) + clamp(2.6rem, 7vh, 4rem); + display: flex; + flex-direction: column; + justify-content: center; } -.eyebrow { - margin-bottom: 16px; - font-size: 12px; - letter-spacing: 0.18em; - opacity: 0.85; -} - -.hero h1 { - margin: 0 0 18px; - font-size: 62px; - line-height: 0.95; +.hero-title { + margin: 0; + font-size: clamp(2.8rem, 8.5vw, 6.4rem); + line-height: 0.88; font-weight: 300; - letter-spacing: -0.04em; - color: white; + letter-spacing: -0.045em; + text-transform: uppercase; + color: #fff; +} + +.hero-title-line { + display: block; + will-change: transform, opacity; +} + +.hero-title-line + .hero-title-line { + margin-top: 0.1em; } .hero-text { - max-width: 320px; - font-size: 15px; - line-height: 1.5; - color: rgba(255, 255, 255, 0.85); + margin-top: 1.25rem; + max-width: 29rem; + font-size: 0.99rem; + line-height: 1.58; + color: rgba(255, 255, 255, 0.86); + will-change: transform, opacity; } .hero-actions { display: flex; + flex-wrap: wrap; gap: 12px; - margin-top: 28px; + margin-top: 1.9rem; + will-change: transform, opacity; } .btn { border: none; border-radius: 999px; - padding: 12px 18px; - font-size: 14px; + padding: 12px 20px; + font-size: 0.9rem; cursor: pointer; - transition: transform 0.2s ease, opacity 0.2s ease; + transition: transform 0.24s ease, opacity 0.24s ease; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; } +.hero .btn { + border-radius: 0; +} + .btn:hover { transform: translateY(-1px); } @@ -86,14 +143,51 @@ } .btn-secondary { - background: rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.16); color: #fff; + border: 1px solid rgba(255, 255, 255, 0.22); backdrop-filter: blur(8px); } +.intro-overlay { + position: absolute; + inset: 0; + z-index: 26; + background: #fff; + display: grid; + place-items: center; + will-change: transform; +} + +.intro-overlay__inner { + width: 100%; + height: 100%; + display: grid; + place-items: center; + padding: clamp(1rem, 4vw, 2.2rem); +} + +.intro-overlay__text-mask { + width: min(96vw, 1200px); + display: flex; + justify-content: center; + overflow: hidden; +} + +.intro-overlay__logo { + width: clamp(100px, 20vw, 340px); + max-width: 92vw; +} + +.intro-overlay__logo img { + width: 100%; + height: auto; + display: block; +} + /* SECTIONS */ .section { - padding: 28px 20px 10px; + padding: 42px 20px 10px; } .section-heading { @@ -123,7 +217,7 @@ overflow: hidden; background: #f5f5f5; border: 1px solid #d9d9d9; - border-radius: 0px; + border-radius: 0; padding: 18px; min-height: 360px; display: flex; @@ -224,7 +318,7 @@ max-width: 600px; height: auto; object-fit: contain; - border-radius: 0px; + border-radius: 0; transition: transform 0.4s ease; } @@ -292,7 +386,7 @@ align-items: center; background: #ff6a00; margin: 20px; - border-radius: 0px; + border-radius: 0; padding: 40px 38px; } @@ -355,18 +449,19 @@ /* RESPONSIVE */ @media (max-width: 900px) { - .hero { - min-height: 620px; - } - .hero-content { - padding: 90px 24px 40px; + width: min(640px, 100%); + padding-top: 7rem; } - .hero h1, + .hero-title, .section-heading h2, .discovery-copy h2 { - font-size: 42px; + font-size: clamp(2.45rem, 9vw, 3.2rem); + } + + .hero-text { + font-size: 0.94rem; } .product-grid { @@ -379,20 +474,36 @@ } @media (max-width: 640px) { - .hero { - margin: 12px; - min-height: 540px; + .hero-brand { + top: 14px; } - .hero h1, + .hero .navbar--hero { + top: 14px; + } + + .hero-content { + padding: 6.2rem 1rem 2.3rem; + } + + .hero-title, .section-heading h2, .discovery-copy h2 { - font-size: 34px; + font-size: clamp(2.05rem, 13vw, 2.7rem); } .hero-actions { flex-direction: column; align-items: flex-start; + width: min(300px, 100%); + } + + .hero-actions .btn { + width: 100%; + } + + .section { + padding: 34px 12px 10px; } .product-grid { @@ -402,4 +513,21 @@ .product-card { min-height: 320px; } -} \ No newline at end of file + + .discovery-section { + margin: 12px; + padding: 28px 20px; + } +} + +@media (prefers-reduced-motion: reduce) { + .hero-media, + .hero-title-line, + .hero-text, + .hero-actions, + .hero-brand, + .intro-overlay { + transition: none !important; + animation: none !important; + } +} diff --git a/parfum-shop/src/pages/LandingPage.jsx b/parfum-shop/src/pages/LandingPage.jsx index 58e87f4..9359d1d 100644 --- a/parfum-shop/src/pages/LandingPage.jsx +++ b/parfum-shop/src/pages/LandingPage.jsx @@ -1,13 +1,162 @@ -import { useEffect, useRef } from "react"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "react"; import { Link } from "react-router"; import { gsap } from "gsap"; +import HeroSection from "../components/landing/HeroSection"; import perfumes from "../data/perfumes"; import "../pages/LandingPage.css"; import "../style/navbar.css"; +const INTRO_SESSION_KEY = "atmos-landing-intro-played"; + function LandingPage() { + const pageRef = useRef(null); + const overlayRef = useRef(null); + const overlayTextRef = useRef(null); + const heroImageWrapRef = useRef(null); + const headlineLineRefs = useRef([]); + const heroMetaRefs = useRef([]); const cardRefs = useRef([]); + 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); + const revealTargets = [...headlineLines, ...heroMeta]; + + if (!overlay || !overlayText || !heroImageWrap || revealTargets.length === 0) { + return undefined; + } + + const ctx = gsap.context(() => { + gsap.set(heroImageWrap, { + scale: 1.22, + transformOrigin: "center center", + }); + + gsap.set(revealTargets, { + y: 56, + autoAlpha: 0, + }); + + if (!shouldPlayIntro) { + gsap.set(heroImageWrap, { scale: 1 }); + gsap.set(revealTargets, { 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, + { + y: 0, + autoAlpha: 1, + duration: 1.04, + stagger: 0.16, + ease: "power3.out", + }, + "<0.27" + ) + .to( + heroMeta, + { + y: 0, + autoAlpha: 1, + duration: 0.98, + stagger: 0.14, + ease: "power3.out", + }, + "<0.2" + ) + .set(overlay, { display: "none" }); + }, pageRef); + + return () => { + ctx.revert(); + }; + }, [shouldPlayIntro]); + useEffect(() => { const cards = cardRefs.current.filter(Boolean); const cardStates = cards @@ -35,7 +184,7 @@ function LandingPage() { try { video.currentTime = 0; } catch { - // Ignore errors when setting currentTime + // Ignore errors when setting currentTime. } }; @@ -45,12 +194,12 @@ function LandingPage() { try { video.currentTime = 0; } catch { - // Ignore errors when setting currentTime + // Ignore errors when setting currentTime. } const playAttempt = video.play(); if (playAttempt && typeof playAttempt.catch === "function") { - playAttempt.catch(() => { }); + playAttempt.catch(() => {}); } }; @@ -150,56 +299,24 @@ function LandingPage() { }, []); return ( -
-
- - -
- -
-

NISCHENDÜFTE

-

- DÜFTE ALS -
- AUSDRUCK -
- VON KONZEPT -

-

- Konzeptuelle Düfte zwischen Materialität, Raum und Charakter. -

- -
- - - Discovery Set - -
-
-
+
+

- WÄHLE EINE + {"W\u00C4HLE EINE"}
- ATMOSPHÄRE + {"ATMOSPH\u00C4RE"}

@@ -209,8 +326,8 @@ function LandingPage() { to={`/duft/${item.slug}`} className="product-card" key={item.id} - ref={(el) => { - cardRefs.current[index] = el; + ref={(element) => { + cardRefs.current[index] = element; }} >
- Discovery Set + Discovery Set
@@ -279,4 +396,4 @@ function LandingPage() { ); } -export default LandingPage; \ No newline at end of file +export default LandingPage;