From 7045ef8bffc1b14adc03351ccac5f05a4aec2b7b Mon Sep 17 00:00:00 2001 From: Ermin Zoronjic Date: Thu, 7 May 2026 01:40:43 +0200 Subject: [PATCH] add product cta sticky interaction --- .../src/components/ProductDetailPage.css | 125 +++++++++++++--- .../src/components/ProductDetailPage.jsx | 134 +++++++++++------- 2 files changed, 188 insertions(+), 71 deletions(-) diff --git a/parfum-shop/src/components/ProductDetailPage.css b/parfum-shop/src/components/ProductDetailPage.css index 3f8b3f3..8822a96 100644 --- a/parfum-shop/src/components/ProductDetailPage.css +++ b/parfum-shop/src/components/ProductDetailPage.css @@ -5,6 +5,15 @@ background: var(--theme-bg); } +.detail-body-grid { + display: grid; + grid-template-columns: 1fr; +} + +.detail-sections { + min-width: 0; +} + .eyebrow, .label-title { font-size: var(--text-xs); @@ -195,24 +204,13 @@ } .product-purchase-panel { - position: relative; - grid-column: 10 / -1; - grid-row: 1; - z-index: 4; - justify-self: end; - align-self: center; - width: min(100%, 390px); - max-height: none; - overflow: visible; display: flex; flex-direction: column; gap: clamp(1rem, 1.8vw, 1.45rem); + width: 100%; padding: 0; border: 0; background: transparent; - backdrop-filter: none; - -webkit-backdrop-filter: none; - box-shadow: none; } .purchase-price-row { @@ -919,6 +917,36 @@ line-height: 1.45; } +@media (min-width: 1024px) { + .detail-body-grid { + grid-template-columns: 1fr clamp(280px, 24vw, 390px); + grid-template-rows: auto 1fr; + column-gap: var(--gap-lg); + } + + .product-hero { + grid-column: 1 / -1; + grid-row: 1; + } + + .detail-sidebar { + grid-column: 2; + grid-row: 1 / 3; + z-index: 4; + padding-top: clamp(10rem, 24svh, 18rem); + } + + .detail-sections { + grid-column: 1; + grid-row: 2; + } + + .product-purchase-panel { + position: sticky; + top: clamp(5rem, 8vw, 6.5rem); + } +} + @media (max-width: 1180px) { .product-story-grid, .product-meta-section, @@ -943,13 +971,9 @@ padding-bottom: clamp(2.5rem, 5vw, 4rem); } - .product-media-column, - .product-purchase-panel { + .product-media-column { grid-column: 1; grid-row: auto; - } - - .product-media-column { min-height: 0; display: grid; grid-template-columns: 1fr; @@ -1021,13 +1045,9 @@ } .product-purchase-panel { - justify-self: stretch; - align-self: auto; width: min(100%, 46rem); max-width: 46rem; - max-height: none; margin-inline: auto; - overflow: visible; gap: var(--gap-sm); } } @@ -1046,8 +1066,7 @@ padding-bottom: clamp(1.5rem, 5vw, 2.5rem); } - .product-media-column, - .product-purchase-panel { + .product-media-column { grid-column: 1; grid-row: auto; } @@ -1230,6 +1249,66 @@ } } +.mobile-purchase-cta { + display: none; +} + +@media (max-width: 1023px) { + .detail-body-grid { + padding-bottom: clamp(4rem, 8vw, 5.5rem); + } + + .mobile-purchase-cta { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 500; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--gap-sm); + padding: clamp(0.6rem, 1.8vw, 0.85rem) var(--page-x); + background: color-mix(in srgb, var(--theme-bg) 88%, transparent); + border-top: 1px solid var(--theme-border); + backdrop-filter: blur(18px) saturate(1.2); + -webkit-backdrop-filter: blur(18px) saturate(1.2); + transform: translateY(100%); + transition: transform var(--duration-med) var(--ease-out); + } + + .mobile-purchase-cta.is-visible { + transform: translateY(0); + } + + .mobile-purchase-info { + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 0; + } + + .mobile-purchase-info span { + color: var(--theme-text-muted); + font-size: var(--text-xs); + letter-spacing: 0.12em; + text-transform: uppercase; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .mobile-purchase-info strong { + color: var(--theme-text); + font-size: var(--text-lg); + font-weight: 400; + } + + .mobile-purchase-cta .atmos-btn { + flex-shrink: 0; + } +} + @media (prefers-reduced-motion: reduce) { .is-transition-arriving .product-hero-image, .is-transition-arriving [data-product-transition-reveal] { diff --git a/parfum-shop/src/components/ProductDetailPage.jsx b/parfum-shop/src/components/ProductDetailPage.jsx index be9c5b3..5222816 100644 --- a/parfum-shop/src/components/ProductDetailPage.jsx +++ b/parfum-shop/src/components/ProductDetailPage.jsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Link, useParams } from "react-router"; import perfumes from "../data/perfumes"; import SharedNavbar from "./SharedNavbar"; @@ -166,17 +166,7 @@ function ProductPurchasePanel({ ); } -function ProductHero({ - perfume, - selectedImage, - setSelectedImage, - selectedSize, - setSelectedSize, - selectedPriceCents, - discountPreviewCents, - addToCart, - subscribeToProduct, -}) { +function ProductHero({ perfume, selectedImage, setSelectedImage }) { const galleryImages = [...new Set([perfume.image, ...(perfume.gallery || [])])].slice(0, 3); return ( @@ -229,15 +219,6 @@ function ProductHero({ - ); } @@ -550,6 +531,33 @@ function ProductRecommendations({ currentSlug, startProductTransition }) { ); } +function MobilePurchaseCTA({ perfume, selectedSize, addToCart, visible }) { + const selectedProductId = `${perfume.slug}-${selectedSize === "sample" ? "sample" : "full"}`; + const selectedProductLabel = selectedSize === "sample" ? "Probe" : "50 ml Flakon"; + + return ( +
+
+ {perfume.name} + {perfume.prices[selectedSize]} +
+ +
+ ); +} + function ProductDetailContent({ perfumeSlug }) { const { addToCart, subscribeToProduct, user } = useShop(); const { activeSlug, phase, startProductTransition } = useProductTransition(); @@ -565,6 +573,8 @@ function ProductDetailContent({ perfumeSlug }) { const [selectedSize, setSelectedSize] = useState("full"); const [showReviewDetails, setShowReviewDetails] = useState(false); const [commentPage, setCommentPage] = useState(0); + const sidebarRef = useRef(null); + const [isPanelVisible, setIsPanelVisible] = useState(true); const selectedPriceCents = priceToCents(perfume.prices[selectedSize]); const sampleCredit = user?.sampleCredits?.find( (credit) => credit.slug === perfume.slug && credit.status === "available" @@ -621,6 +631,17 @@ function ProductDetailContent({ perfumeSlug }) { return () => window.clearInterval(interval); }, [safeCommentPages.length]); + useEffect(() => { + const el = sidebarRef.current; + if (!el) return; + const observer = new IntersectionObserver( + ([entry]) => setIsPanelVisible(entry.isIntersecting), + { threshold: 0 } + ); + observer.observe(el); + return () => observer.disconnect(); + }, []); + const productJsonLd = useMemo(() => buildProductJsonLd(perfume), [perfume]); return ( @@ -638,35 +659,52 @@ function ProductDetailContent({ perfumeSlug }) {
- +
+ - - - - - - +
+ +
+ +
+ + + + + + +
+
+ + ); }