diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 0a01d47..73720ef 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -1,5 +1,5 @@ { - "name": ".parfum_agsd-1", + "name": "parfum_agsd", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package-lock.json b/package-lock.json index de4c189..fa8582c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": ".parfum_agsd-1", + "name": "parfum_agsd", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/parfum-shop/src/App.css b/parfum-shop/src/App.css index 5d8f657..34dff87 100644 --- a/parfum-shop/src/App.css +++ b/parfum-shop/src/App.css @@ -40,41 +40,6 @@ linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.45)); } -/* NAVBAR */ - -.navbar { - position: relative; - z-index: 2; - display: flex; - justify-content: center; - padding-top: 22px; -} - -.nav-pill { - display: flex; - gap: 10px; - padding: 8px 10px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.15); - backdrop-filter: blur(10px); -} - -.nav-link { - text-decoration: none; - color: rgba(255, 255, 255, 0.88); - font-size: 13px; - padding: 8px 14px; - border-radius: 999px; - transition: 0.2s ease; -} - -.nav-link:hover, -.nav-link.active { - background: rgba(255, 255, 255, 0.22); -} - -/* --------------------------------------------------- */ - .hero-content { position: relative; z-index: 2; diff --git a/parfum-shop/src/App.jsx b/parfum-shop/src/App.jsx index c5572bf..cacd0fa 100644 --- a/parfum-shop/src/App.jsx +++ b/parfum-shop/src/App.jsx @@ -1,355 +1,14 @@ -import { useEffect, useRef } from "react"; -import { gsap } from "gsap"; -import "./App.css"; - -// Hallo im Code, -// ich streue hier bewusst Kommentare ein, damit ihr euch nicht auf eine jahrlange -// archäologische Expedition begeben müsst. So bleibt besser sichtbar, wo -// einzelne Sections anfangen und aufhören und wo wichtige Elemente oder -// Funktionen liegen. - -// Euer Freund und Helfer Salih - -// Bei Bugs, kleinen Krisen oder emotionalem Kontrollverlust bitte -// https://stackoverflow.com/questions konsultieren -// oder fragen Sie Salih oder eine KI Ihres Vertrauens. - -//Erreichbar unter salih.hasicic@stud.fhgr.ch oder telefonisch, falls ihr die Nummer habt*/ - -//Elements and Images for the grid -const perfumes = [ - { - id: "01", - name: "KALTER BETON", - image: - "/kalter-beton-product.png", - fillImage: "/platzhalter.png", - fillVideo: "/kalter-beton-hover.webm", - text: "Mineralisch. Roh. Unberührt.", - }, - { - id: "02", - name: "NASSER MARMOR", - image: - "/NASSER MARMOR.png", - fillImage: "/platzhalter.png", - fillVideo: "/nasser-marmor-hover.webm", - text: "Kühl. Glatt. Sinnlich.", - }, - { - id: "03", - name: "BLASSE SEIDE", - image: - "/BLASSE SEIDE.png", - fillImage: "/platzhalter.png", - fillVideo: "/blasse-seide-hover.webm", - text: "Blass. Sanft. Kostbar.", - }, - { - id: "04", - name: "WEISSE ASCHE", - image: - "/WEISSE ASCHE.png", - fillImage: "/platzhalter.png", - fillVideo: "/weisse-asche-hover.webm", - text: "Still. Staubig. Erhaben.", - }, - { - id: "05", - name: "VERBRANNTES CHROM", - image: - "/VERBRANNTES CHROM.png", - fillImage: "/platzhalter.png", - fillVideo: "/verbranntes-chrom-hover.webm", - text: "Metallisch. Verzehrt. Edel.", - }, - { - id: "06", - name: "SCHWARZES BENZIN", - image: - "/SCHWARZES BENZIN.png", - fillImage: "/platzhalter.png", - fillVideo: "/schwarzes-benzin-hover.webm", - text: "Dunkel. Glänzend. Verboten.", - }, -]; +import LandingPage from "./pages/LandingPage"; +import ProductDetailPage from "./components/ProductDetailPage"; function App() { - const cardRefs = useRef([]); + const showDetailPage = true; - 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 (showDetailPage) { + return ; + } - 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 if browser blocks random access while buffering. - } - }; - - const playVideo = (video) => { - if (!video) { - return; - } - - try { - video.currentTime = 0; - } catch { - // Ignore if browser blocks random access while buffering. - } - - 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 ( -
- {/* Hero */} -
- {/* Navbar */} - - {/* --- Navbar End --- */} -
- -
-

NISCHENDÜFTE

-

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

-

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

- -
- - -
-
-
- {/* --- Hero End --- */} - -
- {/* Grid with Core Collection */} -
-
-

- WÄHLE EINE -
- ATMOSPHÄRE -

-
- -
- {perfumes.map((item, index) => ( -
{ - cardRefs.current[index] = el; - }} - > -
- ))} -
-
- {/* --- Grid End --- */} - - {/* Dicovery Set Section */} -
-
-

- DER SICHERE EINSTIEG -
- DISCOVERY SET -

-

- 6 Samples × 2ml. -
- Jeden Duft eine Woche tragen. -
- Verstehen, was funktioniert. -

-
- -
- Discovery Set - -
-
- {/* --- Dicovery Set Section End--- */} -
-
- ); + return ; } -export default App; +export default App; \ No newline at end of file diff --git a/parfum-shop/src/components/ProductDetailPage.css b/parfum-shop/src/components/ProductDetailPage.css new file mode 100644 index 0000000..b1fc4c1 --- /dev/null +++ b/parfum-shop/src/components/ProductDetailPage.css @@ -0,0 +1,432 @@ +/* + Hallo im CSS, + ich versuche auch hier die Struktur sauber zu kommentieren, damit ihr nicht + im Styling-Dschungel verloren geht und schneller versteht, was wohin gehört. + + Bei Bugs, kleinen Krisen oder emotionalem Kontrollverlust bitte + https://stackoverflow.com/questions konsultieren + oder fragt Salih oder eine KI eures Vertrauens. +*/ + +/* --- Product Detail Page Wrapper Start --- */ + +.detail-page { + min-height: 100vh; + background: #efefef; + color: #191919; + padding: 20px; +} + +.detail-shell { + background: #f7f7f7; + border: 1px solid #d6d6d6; + padding: 22px; +} + +/* --- Product Detail Page Wrapper End --- */ + +/* --- Back Link Start --- */ + +.back-link { + background: none; + border: none; + padding: 0; + margin-bottom: 18px; + font-size: 14px; + cursor: pointer; + color: #222; +} + +/* --- Back Link End --- */ + +/* --- Main Detail Layout Start --- */ + +.detail-layout { + display: grid; + grid-template-columns: 1.02fr 1fr; + gap: 28px; + align-items: start; +} + +/* --- Main Detail Layout End --- */ + +/* --- Left Column / Gallery Start --- */ + +.detail-gallery { + display: flex; + flex-direction: column; +} + +.detail-main-image { + background: #ddd; + aspect-ratio: 1 / 1; + overflow: hidden; +} + +.detail-main-image img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.detail-thumbs { + display: flex; + gap: 14px; + margin-top: 14px; +} + +.thumb-btn { + width: 88px; + height: 88px; + border: 1px solid #cfcfcf; + background: #fff; + padding: 0; + cursor: pointer; +} + +.thumb-btn.active { + outline: 2px solid #4da3ff; + outline-offset: 1px; +} + +.thumb-btn img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* --- Left Column / Gallery End --- */ + +/* --- Duftstruktur Start --- */ + +.detail-structure { + margin-top: 28px; +} + +.detail-structure h3 { + font-size: 12px; + letter-spacing: 0.24em; + font-weight: 500; + margin: 0 0 14px; +} + +.structure-block { + margin-bottom: 12px; +} + +.structure-phase { + display: block; + margin-bottom: 6px; + font-size: 10px; + letter-spacing: 0.18em; + color: #555; +} + +.structure-tags { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.structure-tags span { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 8px 14px; + border: 1px solid #d2d2d2; + background: #ebebeb; + font-size: 12px; +} + +.mood-box { + margin-top: 18px; + background: #dfdfdf; + padding: 16px; +} + +.mood-label { + display: block; + margin-bottom: 8px; + font-size: 10px; + letter-spacing: 0.2em; + color: #555; +} + +.mood-box p { + font-size: 15px; + line-height: 1.5; + margin: 0; +} + +/* --- Duftstruktur End --- */ + +/* --- Meta Infos unter Duftstruktur Start --- */ + +.detail-meta-grid { + display: grid; + grid-template-columns: 1fr 1fr 1.5fr; + gap: 20px; + margin-top: 24px; +} + +.detail-meta-grid span { + display: block; + margin-bottom: 8px; + font-size: 10px; + letter-spacing: 0.2em; + color: #666; +} + +.detail-meta-grid p { + font-size: 13px; + line-height: 1.5; + margin: 0; +} + +/* --- Meta Infos unter Duftstruktur End --- */ + +/* --- Right Column Start --- */ + +.detail-info { + display: flex; + flex-direction: column; + gap: 22px; +} + +.detail-heading h1 { + margin: 0 0 6px; + font-size: 52px; + line-height: 0.95; + font-weight: 400; + letter-spacing: -0.04em; + color: #131313; +} + +.detail-heading p { + font-size: 22px; + color: #3f3f3f; + margin: 0; +} + +.detail-section-block { + display: flex; + flex-direction: column; +} + +.label-title { + display: block; + margin-bottom: 10px; + font-size: 11px; + letter-spacing: 0.24em; + color: #666; +} + +/* --- Right Column End --- */ + +/* --- Material Tags Start --- */ + +.material-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.material-tags span { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 8px 14px; + border: 1px solid #d2d2d2; + background: #ebebeb; + font-size: 12px; +} + +/* --- Material Tags End --- */ + +/* --- Size Selection Start --- */ + +.size-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.size-card { + border: 1px solid #d0d0d0; + background: #fefefe; + padding: 18px; + text-align: center; + cursor: pointer; +} + +.size-card.active { + border-color: #111; +} + +.size-title { + display: block; + margin-bottom: 8px; + font-size: 13px; +} + +.size-card strong { + display: block; + font-size: 28px; + font-weight: 400; + margin-bottom: 6px; +} + +.size-card small { + font-size: 11px; + color: #666; +} + +/* --- Size Selection End --- */ + +/* --- Discovery Hinweis + Kaufen Button Start --- */ + +.discovery-note { + background: #050505; + color: #fff; + padding: 14px 16px; +} + +.discovery-note strong { + display: block; + margin-bottom: 6px; + font-size: 13px; +} + +.discovery-note p { + font-size: 11px; + color: rgba(255, 255, 255, 0.78); + margin: 0; +} + +.buy-button { + width: 100%; + border: none; + background: #050505; + color: #fff; + padding: 18px; + font-size: 16px; + letter-spacing: 0.08em; + cursor: pointer; +} + +/* --- Discovery Hinweis + Kaufen Button End --- */ + +/* --- Description Columns Start --- */ + +.detail-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 22px 30px; +} + +.detail-copy-block p { + white-space: pre-line; + color: #2b2b2b; + line-height: 1.55; + margin: 0; +} + +/* --- Description Columns End --- */ + +/* --- Bottom Upsell Bar Start --- */ + +.detail-upsell-bar { + margin-top: auto; + background: #060606; + color: #fff; + padding: 12px 16px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.detail-upsell-bar strong { + display: block; + margin-bottom: 4px; + font-size: 13px; +} + +.detail-upsell-bar p { + font-size: 11px; + color: rgba(255, 255, 255, 0.7); + margin: 0; +} + +/* --- Bottom Upsell Bar End --- */ + +/* --- Bottom CTA Start --- */ + +.detail-bottom-cta { + margin-top: 42px; + padding: 42px 20px 16px; + border-top: 1px solid #d4d4d4; + text-align: center; +} + +.detail-bottom-cta h2 { + margin: 0 0 14px; + font-size: 34px; + font-weight: 400; +} + +.detail-bottom-cta p { + max-width: 880px; + margin: 0 auto; + color: #333; + line-height: 1.7; +} + +.detail-bottom-actions { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 26px; +} + +.detail-bottom-actions button { + border: 1px solid #101010; + background: transparent; + padding: 16px 24px; + min-width: 250px; + cursor: pointer; +} + +/* --- Bottom CTA End --- */ + +/* --- Responsive Start --- */ + +@media (max-width: 1100px) { + .detail-layout { + grid-template-columns: 1fr; + } + + .detail-heading h1 { + font-size: 40px; + } +} + +@media (max-width: 700px) { + .detail-columns, + .detail-meta-grid, + .size-grid, + .detail-bottom-actions { + grid-template-columns: 1fr; + display: grid; + } + + .detail-bottom-actions button { + min-width: 100%; + } + + .detail-thumbs { + flex-wrap: wrap; + } +} + +/* --- Responsive End --- */ \ No newline at end of file diff --git a/parfum-shop/src/components/ProductDetailPage.jsx b/parfum-shop/src/components/ProductDetailPage.jsx new file mode 100644 index 0000000..6a3e418 --- /dev/null +++ b/parfum-shop/src/components/ProductDetailPage.jsx @@ -0,0 +1,232 @@ +import { useMemo, useState } from "react"; +import perfumes from "../data/perfumes"; +import "../navbar.css"; +import "./ProductDetailPage.css"; + +function ProductDetailPage({ perfumeSlug = "kalter-beton" }) { + const perfume = useMemo( + () => perfumes.find((item) => item.slug === perfumeSlug) || perfumes[0], + [perfumeSlug] + ); + + const [selectedImage, setSelectedImage] = useState( + perfume.gallery?.[0] || perfume.image + ); + const [selectedSize, setSelectedSize] = useState("sample"); + + const sizeOptions = [ + { + key: "sample", + title: "Sample 2ml", + price: perfume.prices.sample, + note: "Zum Testen · ca. 20 Anwendungen", + }, + { + key: "full", + title: "Full Size 50ml", + price: perfume.prices.full, + note: "Nachkauf · 500+ Anwendungen", + }, + ]; + + return ( +
+ {/* Navbar */} + + {/* --- Navbar End --- */} + + {/* Product detail content */} +
+ + +
+ {/* Left column */} +
+
+ {perfume.name} +
+ +
+ {[perfume.image, ...(perfume.gallery || [])] + .slice(0, 3) + .map((img, index) => ( + + ))} +
+ +
+

DUFTSTRUKTUR

+ +
+ PHASE 1: TOP NOTES (0–1 H) +
+ {perfume.phases.top.map((note) => ( + {note} + ))} +
+
+ +
+ PHASE 2: HEART NOTES (1–4 H) +
+ {perfume.phases.heart.map((note) => ( + {note} + ))} +
+
+ +
+ PHASE 3: BASE NOTES (4 H+) +
+ {perfume.phases.base.map((note) => ( + {note} + ))} +
+
+ +
+ MOODSETTING +

{perfume.mood}

+
+
+ +
+
+ TRAGEHINWEIS +

{perfume.dosage}

+
+ +
+ HALTBARKEIT +

{perfume.longevity}

+
+ +
+ ANLASS +

{perfume.occasion}

+
+
+
+ + {/* Right column */} +
+
+

{perfume.name}

+

{perfume.shortText}

+
+ +
+ MATERIAL-KOMPOSITION +
+ {perfume.materialTags.map((tag) => ( + {tag} + ))} +
+
+ +
+ GRÖSSE WÄHLEN +
+ {sizeOptions.map((option) => ( + + ))} +
+
+ +
+ Discovery Set wird angerechnet +

+ Hast du das Discovery Set gekauft, wird der volle Preis beim Kauf + automatisch abgezogen. +

+
+ + + +
+
+ BESCHREIBUNG +

{perfume.description}

+
+ +
+ HERKUNFT +

{perfume.origin}

+
+ +
+ KONZENTRATION +

{perfume.concentration}

+
+ +
+ EDITION +

{perfume.edition}

+
+
+ +
+
+ Noch unsicher? +

Discovery Set bestellen und Düfte testen

+
+ +
+
+
+ + {/* Bottom CTA */} +
+

Lieber erst testen?

+

+ Bestelle ein 2ml Sample für CHF 12 oder das komplette Discovery Set + mit allen 6 Düften für CHF 48. Beide werden beim späteren Full-Size-Kauf + vollständig angerechnet. +

+ +
+ + +
+
+
+
+ ); +} + +export default ProductDetailPage; \ No newline at end of file diff --git a/parfum-shop/src/data/perfumes.js b/parfum-shop/src/data/perfumes.js new file mode 100644 index 0000000..b14828f --- /dev/null +++ b/parfum-shop/src/data/perfumes.js @@ -0,0 +1,196 @@ +const perfumes = [ + { + id: "01", + slug: "kalter-beton", + name: "KALTER BETON", + image: "/kalter-beton-product.png", + fillImage: "/platzhalter.png", + fillVideo: "/kalter-beton-hover.webm", + text: "Mineralisch. Roh. Unberührt.", + prices: { + sample: "CHF 12.–", + full: "CHF 185.–", + }, + materialTags: ["Graue Iris", "Stein Akkord", "Zedernholz", "Weihrauch"], + phases: { + top: ["Graue Iris", "Mineral Note", "Betonakkord"], + heart: ["Stein Akkord", "Salz", "Staubnote"], + base: ["Zedernholz", "Pfeffer", "Weihrauch"], + }, + mood: "Unbewohnte Räume. Erste Kälte. Architektonische Stille.", + dosage: "2–3 Sprühstösse", + longevity: "8–12 Stunden", + occasion: "Studio, Konzentration, Arbeit, Beginn, Zurückhaltung.", + description: "Parfümerie / Studio\nAtelier LM / Luz", + origin: "Frankreich, 2024", + concentration: "Eau de Parfum (18%)", + edition: "Batch 04/24 – limitiert 500 Stück", + gallery: [ + "/kalter-beton-product.png", + "/kalter-beton-product.png", + "/kalter-beton-product.png", + ], + }, + { + id: "02", + slug: "nasser-marmor", + name: "NASSER MARMOR", + image: "/NASSER MARMOR.png", + fillImage: "/platzhalter.png", + fillVideo: "/nasser-marmor-hover.webm", + text: "Kühl. Glatt. Sinnlich.", + prices: { + sample: "CHF 12.–", + full: "CHF 185.–", + }, + materialTags: ["Marmorakkord", "Aldehyde", "Moschus", "Vetiver"], + phases: { + top: ["Aldehyde", "Ozonic Note", "Kalter Stein"], + heart: ["Marmorakkord", "Iris", "Nebelnote"], + base: ["Moschus", "Vetiver", "Ambra"], + }, + mood: "Feuchte Flächen. Kühle Eleganz. Polierte Ruhe.", + dosage: "2–3 Sprühstösse", + longevity: "7–11 Stunden", + occasion: "Galerie, Abend, Konzentration, Übergang.", + description: "Parfümerie / Studio\nAtelier LM / Luz", + origin: "Italien, 2024", + concentration: "Eau de Parfum (17%)", + edition: "Batch 02/24 – limitiert 450 Stück", + gallery: [ + "/NASSER MARMOR.png", + "/NASSER MARMOR.png", + "/NASSER MARMOR.png", + ], + }, + { + id: "03", + slug: "blasse-seide", + name: "BLASSE SEIDE", + image: "/BLASSE SEIDE.png", + fillImage: "/platzhalter.png", + fillVideo: "/blasse-seide-hover.webm", + text: "Blass. Sanft. Kostbar.", + prices: { + sample: "CHF 12.–", + full: "CHF 185.–", + }, + materialTags: ["Weisser Moschus", "Reispuder", "Kaschmirholz", "Iris"], + phases: { + top: ["Reispuder", "Helle Blüte", "Luft"], + heart: ["Iris", "Seidenakkord", "Puder"], + base: ["Kaschmirholz", "Weisser Moschus", "Ambrette"], + }, + mood: "Helles Gewebe. Stille Hautnähe. Gedämpfte Wärme.", + dosage: "3–4 Sprühstösse", + longevity: "6–10 Stunden", + occasion: "Alltag, Nähe, Morgen, feine Präsenz.", + description: "Parfümerie / Studio\nAtelier LM / Luz", + origin: "Schweiz, 2024", + concentration: "Eau de Parfum (16%)", + edition: "Batch 03/24 – limitiert 600 Stück", + gallery: [ + "/BLASSE SEIDE.png", + "/BLASSE SEIDE.png", + "/BLASSE SEIDE.png", + ], + }, + { + id: "04", + slug: "weisse-asche", + name: "WEISSE ASCHE", + image: "/WEISSE ASCHE.png", + fillImage: "/platzhalter.png", + fillVideo: "/weisse-asche-hover.webm", + text: "Still. Staubig. Erhaben.", + prices: { + sample: "CHF 12.–", + full: "CHF 185.–", + }, + materialTags: ["Ascheakkord", "Papyrus", "Rauch", "Weisses Holz"], + phases: { + top: ["Staub", "Kalter Rauch", "Papier"], + heart: ["Ascheakkord", "Papyrus", "Kreide"], + base: ["Weisses Holz", "Rauch", "Harz"], + }, + mood: "Stille Rückstände. Helle Spuren. Erhobene Leere.", + dosage: "2–3 Sprühstösse", + longevity: "8–12 Stunden", + occasion: "Abend, Winter, Alleinsein, Konzentration.", + description: "Parfümerie / Studio\nAtelier LM / Luz", + origin: "Deutschland, 2024", + concentration: "Eau de Parfum (19%)", + edition: "Batch 01/24 – limitiert 350 Stück", + gallery: [ + "/WEISSE ASCHE.png", + "/WEISSE ASCHE.png", + "/WEISSE ASCHE.png", + ], + }, + { + id: "05", + slug: "verbranntes-chrom", + name: "VERBRANNTES CHROM", + image: "/VERBRANNTES CHROM.png", + fillImage: "/platzhalter.png", + fillVideo: "/verbranntes-chrom-hover.webm", + text: "Metallisch. Verzehrt. Edel.", + prices: { + sample: "CHF 14.–", + full: "CHF 195.–", + }, + materialTags: ["Metallakkord", "Oud", "Schwarzer Pfeffer", "Labdanum"], + phases: { + top: ["Schwarzer Pfeffer", "Funke", "Metall"], + heart: ["Metallakkord", "Rauch", "Harz"], + base: ["Oud", "Labdanum", "Dunkles Holz"], + }, + mood: "Hitze auf Metall. Dunkler Glanz. Kontrollierte Zerstörung.", + dosage: "1–2 Sprühstösse", + longevity: "10–14 Stunden", + occasion: "Nacht, Statement, Kälte, Formalität.", + description: "Parfümerie / Studio\nAtelier LM / Luz", + origin: "Frankreich, 2024", + concentration: "Extrait de Parfum (24%)", + edition: "Batch 05/24 – limitiert 300 Stück", + gallery: [ + "/VERBRANNTES CHROM.png", + "/VERBRANNTES CHROM.png", + "/VERBRANNTES CHROM.png", + ], + }, + { + id: "06", + slug: "schwarzes-benzin", + name: "SCHWARZES BENZIN", + image: "/SCHWARZES BENZIN.png", + fillImage: "/platzhalter.png", + fillVideo: "/schwarzes-benzin-hover.webm", + text: "Dunkel. Glänzend. Verboten.", + prices: { + sample: "CHF 14.–", + full: "CHF 195.–", + }, + materialTags: ["Petrol Akkord", "Leder", "Birke", "Patchouli"], + phases: { + top: ["Petrol Akkord", "Schwarzer Rauch", "Lack"], + heart: ["Leder", "Birke", "Harz"], + base: ["Patchouli", "Vetiver", "Dunkles Holz"], + }, + mood: "Verbotene Oberfläche. Glanz im Schatten. Asphalt nach Regen.", + dosage: "1–2 Sprühstösse", + longevity: "9–13 Stunden", + occasion: "Abend, Urban, Nacht, Signaturduft.", + description: "Parfümerie / Studio\nAtelier LM / Luz", + origin: "Belgien, 2024", + concentration: "Eau de Parfum (20%)", + edition: "Batch 06/24 – limitiert 280 Stück", + gallery: [ + "/SCHWARZES BENZIN.png", + "/SCHWARZES BENZIN.png", + "/SCHWARZES BENZIN.png", + ], + }, +]; + +export default perfumes; \ No newline at end of file diff --git a/parfum-shop/src/navbar.css b/parfum-shop/src/navbar.css new file mode 100644 index 0000000..ded9081 --- /dev/null +++ b/parfum-shop/src/navbar.css @@ -0,0 +1,76 @@ +/* --- Shared Navbar Start --- */ + +.navbar { + position: relative; + z-index: 20; + display: flex; + justify-content: center; +} + +.nav-pill { + display: flex; + gap: 10px; + padding: 8px 10px; + border-radius: 999px; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); +} + +.nav-link { + text-decoration: none; + font-size: 13px; + padding: 8px 14px; + border-radius: 999px; + transition: 0.2s ease; +} + +/* Hero variant */ +.navbar--hero { + padding-top: 22px; +} + +.navbar--hero .nav-pill { + background: rgba(255, 255, 255, 0.15); +} + +.navbar--hero .nav-link { + color: rgba(255, 255, 255, 0.88); +} + +.navbar--hero .nav-link:hover, +.navbar--hero .nav-link.active { + background: rgba(255, 255, 255, 0.22); +} + +/* Detail page variant */ +.navbar--light { + margin-bottom: 18px; +} + +.navbar--light .nav-pill { + background: rgba(255, 255, 255, 0.88); + border: 1px solid #d6d6d6; +} + +.navbar--light .nav-link { + color: #1d1d1d; +} + +.navbar--light .nav-link:hover, +.navbar--light .nav-link.active { + background: #ebebeb; +} + +/* --- Shared Navbar End --- */ + +@media (max-width: 640px) { + .nav-pill { + gap: 4px; + padding: 6px; + } + + .nav-link { + padding: 8px 10px; + font-size: 12px; + } +} \ No newline at end of file diff --git a/parfum-shop/src/pages/LandingPage.jsx b/parfum-shop/src/pages/LandingPage.jsx new file mode 100644 index 0000000..53f50b9 --- /dev/null +++ b/parfum-shop/src/pages/LandingPage.jsx @@ -0,0 +1,272 @@ +import { useEffect, useRef } from "react"; +import { gsap } from "gsap"; +import perfumes from "../data/perfumes"; +import "../App.css"; +import "../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 {} + }; + + const playVideo = (video) => { + if (!video) return; + + try { + video.currentTime = 0; + } catch {} + + 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 ( +
+
+ + +
+ +
+

NISCHENDÜFTE

+

+ DÜFTE ALS +
+ AUSDRUCK +
+ VON KONZEPT +

+

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

+ +
+ + +
+
+
+ +
+
+
+

+ WÄHLE EINE +
+ ATMOSPHÄRE +

+
+ +
+ {perfumes.map((item, index) => ( +
{ + cardRefs.current[index] = el; + }} + > +
+ ))} +
+
+ +
+
+

+ DER SICHERE EINSTIEG +
+ DISCOVERY SET +

+

+ 6 Samples × 2ml. +
+ Jeden Duft eine Woche tragen. +
+ Verstehen, was funktioniert. +

+
+ +
+ Discovery Set + +
+
+
+
+ ); +} + +export default LandingPage; \ No newline at end of file