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 (
+
+ );
+}
+
+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 (
-
-
+
+
- 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;
}}
>
@@ -244,7 +361,7 @@ function LandingPage() {
))}
@@ -259,7 +376,7 @@ function LandingPage() {
DISCOVERY SET
- Alle 6 Düfte als 2ml Samples 2ml.
+ {"Alle 6 D\u00FCfte als 2ml Samples."}
Jeden Duft eine Woche tragen.
@@ -271,7 +388,7 @@ function LandingPage() {
-
+
@@ -279,4 +396,4 @@ function LandingPage() {
);
}
-export default LandingPage;
\ No newline at end of file
+export default LandingPage;