parfum_agsd/parfum-shop/src/components/ProductDetailPage.jsx

515 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useMemo, useState } from "react";
import { Link, useNavigate, useParams } from "react-router";
import perfumes from "../data/perfumes";
import SharedNavbar from "./SharedNavbar";
import { formatChf } from "../shop/money";
import { useShop } from "../shop/useShop";
import "./ProductDetailPage.css";
const priceToCents = (price) => {
const match = String(price).match(/(\d+)/);
return match ? Number(match[1]) * 100 : 0;
};
function ProductDetailContent({ perfumeSlug }) {
const navigate = useNavigate();
const { addToCart, subscribeToProduct, user } = useShop();
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 [showReviewDetails, setShowReviewDetails] = useState(false);
const [commentPage, setCommentPage] = useState(0);
const [isStructureOpen, setIsStructureOpen] = useState(false);
const [isMoodOpen, setIsMoodOpen] = useState(false);
const selectedProductId = `${perfume.slug}-${selectedSize === "sample" ? "sample" : "full"}`;
const selectedProductLabel = selectedSize === "sample" ? "Sample" : "Full Size";
const selectedPriceCents = priceToCents(perfume.prices[selectedSize]);
const sampleCredit = user?.sampleCredits?.find(
(credit) => credit.slug === perfume.slug && credit.status === "available"
);
const discountPreviewCents =
selectedSize === "full"
? Math.min(
selectedPriceCents,
(user?.discoveryStatus === "Discount available" ? 4800 : 0) +
(sampleCredit?.amount_cents || 0)
)
: 0;
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",
},
];
const reviewSummary = perfume.reviews || {
score: 0,
total: 0,
metrics: [],
};
const reviewComments = perfume.commentSpotlight || [];
const commentPages = [];
for (let i = 0; i < reviewComments.length; i += 2) {
commentPages.push(reviewComments.slice(i, i + 2));
}
const safeCommentPages =
commentPages.length > 0
? commentPages
: [
[
{
id: "fallback-1",
name: "Atelier",
title: "Noch keine Stimmen",
text: "Für diesen Duft sind aktuell noch keine Kommentare hinterlegt.",
},
],
];
useEffect(() => {
const interval = window.setInterval(() => {
setCommentPage((prev) => (prev + 1) % safeCommentPages.length);
}, 5000);
return () => window.clearInterval(interval);
}, [safeCommentPages.length]);
return (
<div className="detail-page">
<SharedNavbar variant="light" />
<main className="detail-shell">
<div className="detail-topbar">
<button className="back-link" type="button" onClick={() => navigate("/")}>
<span className="back-link-arrow"></span>
<span>Zurück zur Startseite</span>
</button>
<div className="detail-topbar-meta">
<span className="detail-topbar-label">DUFTDETAIL</span>
<span className="detail-topbar-name">{perfume.name}</span>
</div>
</div>
<section className="detail-layout">
<div className="detail-gallery">
<div className="detail-main-image">
<img src={selectedImage} alt={perfume.name} decoding="async" />
</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}`}
loading="lazy"
decoding="async"
/>
</button>
))}
</div>
<div className="detail-meta-grid detail-meta-grid--top">
<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>
{/* --- Accordion Group Start --- */}
<div className="accordion-group">
{/* Dropdown: Duftstruktur */}
<div className={`accordion-item ${isStructureOpen ? "is-open" : ""}`}>
<button
type="button"
className="accordion-toggle"
onClick={() => setIsStructureOpen(!isStructureOpen)}
>
<span>DUFTSTRUKTUR</span>
<span className="accordion-icon">{isStructureOpen ? "" : "+"}</span>
</button>
{isStructureOpen && (
<div className="accordion-content">
<div className="detail-structure-layout">
<div className="detail-structure">
<div className="structure-block">
<span className="structure-phase">PHASE 1: TOP NOTES (01 H)</span>
<div className="structure-tags-grid">
{perfume.phases.top.map((note) => (
<span key={note} className="structure-tag">{note}</span>
))}
</div>
</div>
<div className="structure-block">
<span className="structure-phase">PHASE 2: HEART NOTES (14 H)</span>
<div className="structure-tags-grid">
{perfume.phases.heart.map((note) => (
<span key={note} className="structure-tag">{note}</span>
))}
</div>
</div>
<div className="structure-block">
<span className="structure-phase">PHASE 3: BASE NOTES (4 H+)</span>
<div className="structure-tags-grid">
{perfume.phases.base.map((note) => (
<span key={note} className="structure-tag">{note}</span>
))}
</div>
</div>
</div>
<aside className="structure-info-box">
<span className="structure-info-label">ZUR EINORDNUNG</span>
<p>
Die Duftstruktur zeigt, wie sich der Duft über die Zeit entfaltet:
der erste Eindruck im Auftakt, die eigentliche Signatur im Herzen
und die Spur, die lange auf Haut und Kleidung bleibt.
</p>
<div className="structure-info-legend">
<div>
<span className="structure-info-dot structure-info-dot--light" />
<span>Auftakt</span>
</div>
<div>
<span className="structure-info-dot structure-info-dot--mid" />
<span>Herz</span>
</div>
<div>
<span className="structure-info-dot structure-info-dot--strong" />
<span>Basis</span>
</div>
</div>
</aside>
</div>
</div>
)}
</div>
{/* Dropdown: Moodsetting */}
<div className={`accordion-item ${isMoodOpen ? "is-open" : ""}`}>
<button
type="button"
className="accordion-toggle"
onClick={() => setIsMoodOpen(!isMoodOpen)}
>
<span>MOODSETTING</span>
<span className="accordion-icon">{isMoodOpen ? "" : "+"}</span>
</button>
{isMoodOpen && (
<div className="accordion-content">
<div className="mood-box-content">
<p>{perfume.mood}</p>
</div>
</div>
)}
</div>
</div>
{/* --- Accordion Group End --- */}
</div>
<div className="detail-info">
<div className="detail-heading" data-reveal-group>
<div className="detail-heading-copy">
<span className="detail-kicker" data-reveal="fade">
Edition 04
</span>
<h1 data-reveal="fade">{perfume.name}</h1>
<p data-reveal="fade">{perfume.shortText}</p>
</div>
</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" data-reveal-group data-reveal-start="top 88%">
<div className="discovery-note-text" data-reveal="fade">
<strong>Discovery Set wird einmalig angerechnet</strong>
<p>
Nur das erste Discovery Set erzeugt CHF 48 Guthaben. Es wird
einmal bei einem späteren Full-Size-Kauf automatisch abgezogen.
</p>
{discountPreviewCents > 0 && (
<p className="discount-preview">
Erwarteter Preis mit Rabatt:{" "}
<strong>{formatChf(selectedPriceCents - discountPreviewCents)}</strong>
</p>
)}
</div>
<Link to="/discovery-set" className="discovery-note-btn" data-reveal="fade">
Zum Set
</Link>
</div>
<button
className="buy-button"
type="button"
onClick={() =>
addToCart(
selectedProductId,
1,
`${perfume.name} ${selectedProductLabel} added.`
).catch(() => {})
}
>
KAUFEN
</button>
<button
className="restock-button"
type="button"
onClick={() => subscribeToProduct(selectedProductId, "restock").catch(() => {})}
>
RESTOCK UPDATE ABONNIEREN
</button>
<div className="detail-description-section">
<span className="label-title">BESCHREIBUNG</span>
<div className="detail-columns">
<div className="detail-copy-block">
<span className="detail-copy-label">PARFÜMERIE / STUDIO</span>
<p>{perfume.description}</p>
</div>
<div className="detail-copy-block">
<span className="detail-copy-label">HERKUNFT</span>
<p>{perfume.origin}</p>
</div>
<div className="detail-copy-block">
<span className="detail-copy-label">KONZENTRATION</span>
<p>{perfume.concentration}</p>
</div>
<div className="detail-copy-block">
<span className="detail-copy-label">EDITION</span>
<p>{perfume.edition}</p>
</div>
</div>
</div>
<div className="delivery-box">
<div className="delivery-box-header">
<span className="label-title">LIEFERUNG</span>
<span className="delivery-badge">CH</span>
</div>
<div className="delivery-grid">
<div className="delivery-item">
<span className="delivery-item-label">VERSAND</span>
<p>Innerhalb von 12 Werktagen</p>
</div>
<div className="delivery-item">
<span className="delivery-item-label">ZUSTELLUNG</span>
<p>In der Regel in 56 Tagen bei dir</p>
</div>
<div className="delivery-item delivery-item--full">
<span className="delivery-item-label">HINWEIS</span>
<p>Sorgfältig verpackt und geschützt versendet.</p>
</div>
</div>
</div>
<div className="comment-spotlight">
<div className="comment-spotlight-header">
<span className="label-title">STIMMEN ZUM DUFT</span>
<div className="comment-dots">
{safeCommentPages.map((_, index) => (
<button
key={index}
type="button"
className={`comment-dot ${commentPage === index ? "active" : ""}`}
onClick={() => setCommentPage(index)}
aria-label={`Kommentargruppe ${index + 1}`}
/>
))}
</div>
</div>
<div className="comment-spotlight-grid">
{safeCommentPages[commentPage].map((comment) => (
<article className="comment-card" key={comment.id}>
<span className="comment-card-title">{comment.title}</span>
<p>{comment.text}</p>
<span className="comment-card-author">{comment.name}</span>
</article>
))}
</div>
</div>
<div className="review-section">
<div className="review-section-header">
<div className="review-section-copy">
<span className="label-title">RESONANZ</span>
<p className="review-section-text">
Verdichtete Wahrnehmung aus bisherigen Stimmen zu Charakter,
Haltbarkeit, Sillage und Originalität.
</p>
</div>
<div className="review-section-main">
<span className="review-score">{reviewSummary.score.toFixed(1)}</span>
<div className="review-summary-copy">
<span className="review-stars"></span>
<span className="review-count">{reviewSummary.total} Stimmen</span>
</div>
</div>
</div>
<div className={`review-panel ${showReviewDetails ? "is-open" : ""}`}>
<button
type="button"
className="review-toggle"
onClick={() => setShowReviewDetails((prev) => !prev)}
aria-expanded={showReviewDetails}
>
<span>Detailbewertungen</span>
<span className={showReviewDetails ? "review-toggle-icon open" : "review-toggle-icon"}>
+
</span>
</button>
{showReviewDetails && (
<div className="review-popover">
<div className="review-details">
{reviewSummary.metrics.map((metric) => (
<div className="review-detail-row" key={metric.label}>
<span className="review-detail-label">{metric.label}</span>
<div className="review-detail-bar">
<div
className="review-detail-fill"
style={{ width: `${(metric.value / 5) * 100}%` }}
/>
</div>
<span className="review-detail-value">
{metric.value.toFixed(1)}
</span>
</div>
))}
<button
type="button"
className="review-write-button"
disabled
title="Später mit Login verfügbar"
>
Bewertung schreiben
</button>
</div>
</div>
)}
</div>
</div>
</div>
</section>
<section className="detail-bottom-cta" data-reveal-group>
<h2 data-reveal="fade">Lieber erst testen?</h2>
<p data-reveal="fade">
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" data-reveal="fade">
<button
type="button"
onClick={() =>
addToCart(`${perfume.slug}-sample`, 1, `${perfume.name} Sample added.`).catch(() => {})
}
>
SAMPLE BESTELLEN {perfume.prices.sample}
</button>
<button
type="button"
onClick={() => addToCart("discovery-set", 1, "Discovery Set added.").catch(() => {})}
>
DISCOVERY SET CHF 48
</button>
</div>
</section>
</main>
</div>
);
}
function ProductDetailPage() {
const { perfumeSlug = "kalter-beton" } = useParams();
return <ProductDetailContent key={perfumeSlug} perfumeSlug={perfumeSlug} />;
}
export default ProductDetailPage;