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,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": ".parfum_agsd-1",
|
||||
"name": "parfum_agsd",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 <ProductDetailPage perfumeSlug="kalter-beton" />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
return <LandingPage />;
|
||||
}
|
||||
|
||||
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