add productdetailpage
This commit is contained in:
parent
6479792acf
commit
624c8c8f1a
2
node_modules/.package-lock.json
generated
vendored
2
node_modules/.package-lock.json
generated
vendored
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": ".parfum_agsd-1",
|
"name": "parfum_agsd",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": ".parfum_agsd-1",
|
"name": "parfum_agsd",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@ -40,41 +40,6 @@
|
|||||||
linear-gradient(to bottom, rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.45));
|
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 {
|
.hero-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
|||||||
@ -1,355 +1,14 @@
|
|||||||
import { useEffect, useRef } from "react";
|
import LandingPage from "./pages/LandingPage";
|
||||||
import { gsap } from "gsap";
|
import ProductDetailPage from "./components/ProductDetailPage";
|
||||||
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.",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const cardRefs = useRef([]);
|
const showDetailPage = true;
|
||||||
|
|
||||||
useEffect(() => {
|
if (showDetailPage) {
|
||||||
const cards = cardRefs.current.filter(Boolean);
|
return <ProductDetailPage perfumeSlug="kalter-beton" />;
|
||||||
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 <LandingPage />;
|
||||||
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 (
|
|
||||||
<div className="page">
|
|
||||||
{/* Hero */}
|
|
||||||
<header className="hero">
|
|
||||||
{/* Navbar */}
|
|
||||||
<nav className="navbar">
|
|
||||||
<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>
|
|
||||||
{/* --- Navbar End --- */}
|
|
||||||
<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>
|
|
||||||
{/* --- Hero End --- */}
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{/* Grid with Core Collection */}
|
|
||||||
<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) => (
|
|
||||||
<article
|
|
||||||
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>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/* --- Grid End --- */}
|
|
||||||
|
|
||||||
{/* Dicovery Set Section */}
|
|
||||||
<section className="discovery-section" id="testen">
|
|
||||||
<div className="discovery-copy">
|
|
||||||
<h2>
|
|
||||||
DER SICHERE EINSTIEG
|
|
||||||
<br />
|
|
||||||
DISCOVERY SET
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
6 Samples × 2ml.
|
|
||||||
<br />
|
|
||||||
Jeden Duft eine Woche tragen.
|
|
||||||
<br />
|
|
||||||
Verstehen, was funktioniert.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="discovery-banner">
|
|
||||||
<img
|
|
||||||
src="/DISCOVERYSET.png"
|
|
||||||
alt="Discovery Set"
|
|
||||||
/>
|
|
||||||
<button className="banner-btn">Discovery Set bestellen</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/* --- Dicovery Set Section End--- */}
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
432
parfum-shop/src/components/ProductDetailPage.css
Normal file
432
parfum-shop/src/components/ProductDetailPage.css
Normal file
@ -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 --- */
|
||||||
232
parfum-shop/src/components/ProductDetailPage.jsx
Normal file
232
parfum-shop/src/components/ProductDetailPage.jsx
Normal file
@ -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 (
|
||||||
|
<div className="detail-page">
|
||||||
|
{/* Navbar */}
|
||||||
|
<nav className="navbar navbar--light">
|
||||||
|
<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>
|
||||||
|
{/* --- Navbar End --- */}
|
||||||
|
|
||||||
|
{/* Product detail content */}
|
||||||
|
<main className="detail-shell">
|
||||||
|
<button className="back-link" type="button">
|
||||||
|
← Zurück zur Startseite
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<section className="detail-layout">
|
||||||
|
{/* Left column */}
|
||||||
|
<div className="detail-gallery">
|
||||||
|
<div className="detail-main-image">
|
||||||
|
<img src={selectedImage} alt={perfume.name} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-thumbs">
|
||||||
|
{[perfume.image, ...(perfume.gallery || [])]
|
||||||
|
.slice(0, 3)
|
||||||
|
.map((img, index) => (
|
||||||
|
<button
|
||||||
|
key={`${img}-${index}`}
|
||||||
|
type="button"
|
||||||
|
className={`thumb-btn ${selectedImage === img ? "active" : ""}`}
|
||||||
|
onClick={() => setSelectedImage(img)}
|
||||||
|
>
|
||||||
|
<img src={img} alt={`${perfume.name} Ansicht ${index + 1}`} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-structure">
|
||||||
|
<h3>DUFTSTRUKTUR</h3>
|
||||||
|
|
||||||
|
<div className="structure-block">
|
||||||
|
<span className="structure-phase">PHASE 1: TOP NOTES (0–1 H)</span>
|
||||||
|
<div className="structure-tags">
|
||||||
|
{perfume.phases.top.map((note) => (
|
||||||
|
<span key={note}>{note}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="structure-block">
|
||||||
|
<span className="structure-phase">PHASE 2: HEART NOTES (1–4 H)</span>
|
||||||
|
<div className="structure-tags">
|
||||||
|
{perfume.phases.heart.map((note) => (
|
||||||
|
<span key={note}>{note}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="structure-block">
|
||||||
|
<span className="structure-phase">PHASE 3: BASE NOTES (4 H+)</span>
|
||||||
|
<div className="structure-tags">
|
||||||
|
{perfume.phases.base.map((note) => (
|
||||||
|
<span key={note}>{note}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mood-box">
|
||||||
|
<span className="mood-label">MOODSETTING</span>
|
||||||
|
<p>{perfume.mood}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-meta-grid">
|
||||||
|
<div>
|
||||||
|
<span>TRAGEHINWEIS</span>
|
||||||
|
<p>{perfume.dosage}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>HALTBARKEIT</span>
|
||||||
|
<p>{perfume.longevity}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>ANLASS</span>
|
||||||
|
<p>{perfume.occasion}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right column */}
|
||||||
|
<div className="detail-info">
|
||||||
|
<div className="detail-heading">
|
||||||
|
<h1>{perfume.name}</h1>
|
||||||
|
<p>{perfume.shortText}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-section-block">
|
||||||
|
<span className="label-title">MATERIAL-KOMPOSITION</span>
|
||||||
|
<div className="material-tags">
|
||||||
|
{perfume.materialTags.map((tag) => (
|
||||||
|
<span key={tag}>{tag}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-section-block">
|
||||||
|
<span className="label-title">GRÖSSE WÄHLEN</span>
|
||||||
|
<div className="size-grid">
|
||||||
|
{sizeOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.key}
|
||||||
|
type="button"
|
||||||
|
className={`size-card ${selectedSize === option.key ? "active" : ""}`}
|
||||||
|
onClick={() => setSelectedSize(option.key)}
|
||||||
|
>
|
||||||
|
<span className="size-title">{option.title}</span>
|
||||||
|
<strong>{option.price}</strong>
|
||||||
|
<small>{option.note}</small>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discovery-note">
|
||||||
|
<strong>Discovery Set wird angerechnet</strong>
|
||||||
|
<p>
|
||||||
|
Hast du das Discovery Set gekauft, wird der volle Preis beim Kauf
|
||||||
|
automatisch abgezogen.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="buy-button" type="button">
|
||||||
|
KAUFEN
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="detail-columns">
|
||||||
|
<div className="detail-copy-block">
|
||||||
|
<span className="label-title">BESCHREIBUNG</span>
|
||||||
|
<p>{perfume.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-copy-block">
|
||||||
|
<span className="label-title">HERKUNFT</span>
|
||||||
|
<p>{perfume.origin}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-copy-block">
|
||||||
|
<span className="label-title">KONZENTRATION</span>
|
||||||
|
<p>{perfume.concentration}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-copy-block">
|
||||||
|
<span className="label-title">EDITION</span>
|
||||||
|
<p>{perfume.edition}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="detail-upsell-bar">
|
||||||
|
<div>
|
||||||
|
<strong>Noch unsicher?</strong>
|
||||||
|
<p>Discovery Set bestellen und Düfte testen</p>
|
||||||
|
</div>
|
||||||
|
<span>—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Bottom CTA */}
|
||||||
|
<section className="detail-bottom-cta">
|
||||||
|
<h2>Lieber erst testen?</h2>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="detail-bottom-actions">
|
||||||
|
<button type="button">SAMPLE BESTELLEN – CHF 12</button>
|
||||||
|
<button type="button">DISCOVERY SET – CHF 48</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProductDetailPage;
|
||||||
196
parfum-shop/src/data/perfumes.js
Normal file
196
parfum-shop/src/data/perfumes.js
Normal file
@ -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;
|
||||||
76
parfum-shop/src/navbar.css
Normal file
76
parfum-shop/src/navbar.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
272
parfum-shop/src/pages/LandingPage.jsx
Normal file
272
parfum-shop/src/pages/LandingPage.jsx
Normal file
@ -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 (
|
||||||
|
<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) => (
|
||||||
|
<article
|
||||||
|
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>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="discovery-section" id="testen">
|
||||||
|
<div className="discovery-copy">
|
||||||
|
<h2>
|
||||||
|
DER SICHERE EINSTIEG
|
||||||
|
<br />
|
||||||
|
DISCOVERY SET
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
6 Samples × 2ml.
|
||||||
|
<br />
|
||||||
|
Jeden Duft eine Woche tragen.
|
||||||
|
<br />
|
||||||
|
Verstehen, was funktioniert.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="discovery-banner">
|
||||||
|
<img src="/DISCOVERYSET.png" alt="Discovery Set" />
|
||||||
|
<button className="banner-btn">Discovery Set bestellen</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LandingPage;
|
||||||
Loading…
x
Reference in New Issue
Block a user