Add fixed categories dock and specials-driven home carousels.
Dock links use the six API category slugs so storefront navigation stays connected to catalog filters. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
35939e6ee4
commit
6c060e222c
@@ -4,6 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="enamad" content="16489694" />
|
||||||
<title>شیرینی بلوط - صفحه اصلی</title>
|
<title>شیرینی بلوط - صفحه اصلی</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 MiB |
@@ -3,6 +3,7 @@ import Header from './components/Header'
|
|||||||
import Footer from './components/Footer'
|
import Footer from './components/Footer'
|
||||||
import ToastHost from './components/ToastHost'
|
import ToastHost from './components/ToastHost'
|
||||||
import CartDrawer from './components/CartDrawer'
|
import CartDrawer from './components/CartDrawer'
|
||||||
|
import CategoriesDock from './components/CategoriesDock'
|
||||||
import HomePage from './pages/HomePage'
|
import HomePage from './pages/HomePage'
|
||||||
import QuickInfoPage from './pages/QuickInfoPage'
|
import QuickInfoPage from './pages/QuickInfoPage'
|
||||||
import ContactUsPage from './pages/ContactUsPage'
|
import ContactUsPage from './pages/ContactUsPage'
|
||||||
@@ -26,6 +27,7 @@ export default function App() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
<CategoriesDock />
|
||||||
<ToastHost />
|
<ToastHost />
|
||||||
<CartDrawer />
|
<CartDrawer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
.categories-dock {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 18px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 80;
|
||||||
|
width: min(var(--max-container), calc(100vw - 48px));
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-radius: 28px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
165deg,
|
||||||
|
rgba(255, 255, 255, 0.72) 0%,
|
||||||
|
rgba(255, 247, 241, 0.55) 45%,
|
||||||
|
rgba(255, 255, 255, 0.4) 100%
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(18px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(18px) saturate(160%);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 36px rgba(143, 65, 12, 0.14),
|
||||||
|
0 2px 8px rgba(0, 0, 0, 0.04),
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.85),
|
||||||
|
inset 0 -1px 0 rgba(143, 65, 12, 0.04);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__list {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__item {
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__link {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__link:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__image-wrap {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: clamp(52px, 5.5vw, 68px);
|
||||||
|
height: clamp(52px, 5.5vw, 68px);
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__names {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__name-fa {
|
||||||
|
font-size: clamp(13px, 1.4vw, 16px);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__name-en {
|
||||||
|
font-family: Cailyne, serif;
|
||||||
|
font-size: clamp(11px, 1.2vw, 13px);
|
||||||
|
font-weight: 300;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.categories-dock {
|
||||||
|
width: calc(100vw - 24px);
|
||||||
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__list {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__link {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__name-en {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.categories-dock {
|
||||||
|
bottom: 10px;
|
||||||
|
width: calc(100vw - 12px);
|
||||||
|
padding: 8px 8px;
|
||||||
|
border-radius: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__list {
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__link {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__image-wrap {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__names {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__name-fa {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.categories-dock__name-en {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import './CategoriesDock.css'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fixed glossy dock — images and labels are hardcoded.
|
||||||
|
* Each item links to `/products/:slug` matching the API category slug.
|
||||||
|
*/
|
||||||
|
const DOCK_CATEGORIES = [
|
||||||
|
{
|
||||||
|
nameFa: 'کیک',
|
||||||
|
nameEn: 'Cake',
|
||||||
|
image: '/categories/Cake.png',
|
||||||
|
slug: 'cake',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'شیرینی',
|
||||||
|
nameEn: 'Pastry',
|
||||||
|
image: '/categories/pastry.png',
|
||||||
|
slug: 'pastry',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'سوهان',
|
||||||
|
nameEn: 'Sohan',
|
||||||
|
image: '/categories/sohan.png',
|
||||||
|
slug: 'sohan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'دسر',
|
||||||
|
nameEn: 'Dessert',
|
||||||
|
image: '/categories/desert.png',
|
||||||
|
slug: 'desert',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'شکلات',
|
||||||
|
nameEn: 'Chocolate',
|
||||||
|
image: '/categories/Choclate.png',
|
||||||
|
slug: 'chocolate',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameFa: 'باقلوا',
|
||||||
|
nameEn: 'Baklava',
|
||||||
|
image: '/categories/baghlava.png',
|
||||||
|
slug: 'baklava',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export default function CategoriesDock() {
|
||||||
|
return (
|
||||||
|
<nav className="categories-dock" aria-label="دستهبندی محصولات">
|
||||||
|
<ul className="categories-dock__list">
|
||||||
|
{DOCK_CATEGORIES.map((category) => (
|
||||||
|
<li key={category.slug} className="categories-dock__item">
|
||||||
|
<Link
|
||||||
|
to={`/products/${category.slug}`}
|
||||||
|
className="categories-dock__link"
|
||||||
|
>
|
||||||
|
<span className="categories-dock__image-wrap">
|
||||||
|
<img src={category.image} alt="" className="categories-dock__image" />
|
||||||
|
</span>
|
||||||
|
<span className="categories-dock__names">
|
||||||
|
<span className="categories-dock__name-fa">{category.nameFa}</span>
|
||||||
|
<span className="categories-dock__name-en">{category.nameEn}</span>
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
.image-lightbox {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1100;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border: none;
|
||||||
|
background: rgba(20, 12, 8, 0.78);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__close {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
inset-inline-end: 18px;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: var(--color-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: var(--color-primary);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__nav--prev {
|
||||||
|
inset-inline-start: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__nav--next {
|
||||||
|
inset-inline-end: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__stage {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
max-width: min(920px, 100%);
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__image {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(100vh - 48px);
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__counter {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 12px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
margin: 0;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.image-lightbox {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__nav--prev {
|
||||||
|
inset-inline-start: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-lightbox__nav--next {
|
||||||
|
inset-inline-end: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import { ChevronLeft, ChevronRight, X } from 'lucide-react'
|
||||||
|
import './ImageLightbox.css'
|
||||||
|
|
||||||
|
type ImageLightboxProps = {
|
||||||
|
images: string[]
|
||||||
|
index: number
|
||||||
|
alt?: string
|
||||||
|
onClose: () => void
|
||||||
|
onIndexChange: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImageLightbox({
|
||||||
|
images,
|
||||||
|
index,
|
||||||
|
alt = '',
|
||||||
|
onClose,
|
||||||
|
onIndexChange,
|
||||||
|
}: ImageLightboxProps) {
|
||||||
|
const safeIndex = ((index % images.length) + images.length) % images.length
|
||||||
|
const current = images[safeIndex]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onKey(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Escape') onClose()
|
||||||
|
if (event.key === 'ArrowLeft') onIndexChange(safeIndex + 1)
|
||||||
|
if (event.key === 'ArrowRight') onIndexChange(safeIndex - 1)
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKey)
|
||||||
|
const prevOverflow = document.body.style.overflow
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', onKey)
|
||||||
|
document.body.style.overflow = prevOverflow
|
||||||
|
}
|
||||||
|
}, [onClose, onIndexChange, safeIndex])
|
||||||
|
|
||||||
|
if (!current) return null
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div className="image-lightbox" role="dialog" aria-modal="true" aria-label="گالری تصاویر">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-lightbox__backdrop"
|
||||||
|
aria-label="بستن"
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-lightbox__close"
|
||||||
|
aria-label="بستن"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<X size={22} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{images.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-lightbox__nav image-lightbox__nav--prev"
|
||||||
|
aria-label="قبلی"
|
||||||
|
onClick={() => onIndexChange(safeIndex + 1)}
|
||||||
|
>
|
||||||
|
<ChevronRight size={22} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="image-lightbox__nav image-lightbox__nav--next"
|
||||||
|
aria-label="بعدی"
|
||||||
|
onClick={() => onIndexChange(safeIndex - 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeft size={22} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="image-lightbox__stage">
|
||||||
|
<img src={current} alt={alt} className="image-lightbox__image" />
|
||||||
|
{images.length > 1 && (
|
||||||
|
<p className="image-lightbox__counter" dir="ltr">
|
||||||
|
{safeIndex + 1} / {images.length}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
.store-card {
|
.store-card {
|
||||||
display: block;
|
display: block;
|
||||||
background-color: rgba(143, 65, 12, 0.05);
|
background-color: rgba(143, 65, 12, 0.05);
|
||||||
border-radius: 100px 100px 25px 25px;
|
border-radius: 100px 100px 22px 22px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
transition: transform 0.2s ease, background-color 0.2s ease;
|
transition: transform 0.2s ease, background-color 0.2s ease;
|
||||||
}
|
}
|
||||||
@@ -16,26 +17,28 @@
|
|||||||
.store-card__image {
|
.store-card__image {
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: 88px 88px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__image img {
|
.store-card__image img {
|
||||||
border-radius: 90px 90px 0 0;
|
border-radius: 88px 88px 0 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
object-position: center;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__description {
|
.store-card__description {
|
||||||
margin-top: 7px;
|
margin-top: 10px;
|
||||||
padding-inline: 4px 8px;
|
padding-inline: 8px 10px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__title {
|
.store-card__title {
|
||||||
color: var(--color-gray-400);
|
color: var(--color-gray-400);
|
||||||
font-size: clamp(13px, 3vw, 18px);
|
font-size: clamp(12px, 2.2vw, 14px);
|
||||||
min-height: 48px;
|
min-height: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
@@ -43,57 +46,69 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.store-card__footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.store-card__subtitle {
|
.store-card__subtitle {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
font-size: clamp(15px, 3vw, 20px);
|
font-size: clamp(12px, 2.2vw, 15px);
|
||||||
margin-top: 4px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__subtitle-detail {
|
.store-card__subtitle-detail {
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
font-size: clamp(8px, 2vw, 10px);
|
font-size: clamp(8px, 1.6vw, 9px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__subtitle-weight {
|
.store-card__subtitle-weight {
|
||||||
color: rgba(94, 94, 94, 0.5);
|
color: rgba(94, 94, 94, 0.5);
|
||||||
font-size: clamp(10px, 2vw, 13px);
|
font-size: clamp(9px, 1.8vw, 11px);
|
||||||
}
|
|
||||||
|
|
||||||
.store-card__actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__arrow {
|
.store-card__arrow {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 678px) {
|
@media (max-width: 678px) {
|
||||||
.store-card {
|
.store-card {
|
||||||
border-radius: 60px 60px 15px 15px;
|
border-radius: 60px 60px 16px 16px;
|
||||||
padding: 7px;
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card__image {
|
||||||
|
border-radius: 52px 52px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__image img {
|
.store-card__image img {
|
||||||
border-radius: 50px 50px 0 0;
|
border-radius: 52px 52px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card__description {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-inline: 6px 8px;
|
||||||
|
padding-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__title {
|
.store-card__title {
|
||||||
min-height: 0;
|
font-size: 12px;
|
||||||
font-size: 13px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__subtitle {
|
.store-card__subtitle {
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.store-card__actions {
|
.store-card__arrow {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ export default function ProductCard({ product }: ProductCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="store-card__description">
|
<div className="store-card__description">
|
||||||
<div className="store-card__title">{product.nameFa}</div>
|
<div className="store-card__title">{product.nameFa}</div>
|
||||||
|
<div className="store-card__footer">
|
||||||
<div className="store-card__subtitle">
|
<div className="store-card__subtitle">
|
||||||
<span>{price.amount}</span>
|
<span>{price.amount}</span>
|
||||||
<span className="store-card__subtitle-detail"> تومان</span>
|
<span className="store-card__subtitle-detail"> تومان</span>
|
||||||
<span className="store-card__subtitle-weight"> /{price.unitLabel}</span>
|
<span className="store-card__subtitle-weight"> /{price.unitLabel}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="store-card__actions">
|
|
||||||
<span className="store-card__arrow" aria-hidden="true">
|
<span className="store-card__arrow" aria-hidden="true">
|
||||||
<ChevronLeft size={18} />
|
<ChevronLeft size={18} />
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
.product-carousel {
|
||||||
|
margin-top: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel--framed {
|
||||||
|
margin-top: 96px;
|
||||||
|
padding-block: 52px;
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(143, 65, 12, 0.055) 0%,
|
||||||
|
rgba(143, 65, 12, 0.028) 48%,
|
||||||
|
rgba(143, 65, 12, 0.055) 100%
|
||||||
|
);
|
||||||
|
border-top: 1px solid rgba(143, 65, 12, 0.16);
|
||||||
|
border-bottom: 1px solid rgba(143, 65, 12, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Title (1 slot) + 4 visible product cards = 5 equal columns */
|
||||||
|
.product-carousel__row {
|
||||||
|
--carousel-gap: 8px;
|
||||||
|
--carousel-slots: 5;
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--carousel-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__aside {
|
||||||
|
flex: 0 0
|
||||||
|
calc(
|
||||||
|
(100% - (var(--carousel-slots) - 1) * var(--carousel-gap)) / var(--carousel-slots)
|
||||||
|
);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 14px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__title {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__aside .size-lg .header-title__placeholder {
|
||||||
|
font-size: clamp(40px, 6.5vw, 68px);
|
||||||
|
line-height: 0.9;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__aside .size-lg .header-title__primary {
|
||||||
|
font-size: clamp(20px, 2.8vw, 32px);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__aside .size-lg .header-title__secondary {
|
||||||
|
font-size: clamp(13px, 1.8vw, 17px);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__all {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.4;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__all::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
width: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: currentColor;
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__all:hover::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__stage {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__nav {
|
||||||
|
position: absolute;
|
||||||
|
top: 42%;
|
||||||
|
z-index: 2;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid rgba(143, 65, 12, 0.18);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: var(--color-primary);
|
||||||
|
box-shadow: 0 2px 10px rgba(75, 75, 75, 0.08);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__nav:hover {
|
||||||
|
background: #fff;
|
||||||
|
border-color: rgba(143, 65, 12, 0.4);
|
||||||
|
box-shadow: 0 4px 14px rgba(75, 75, 75, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Over first / last visible product cards */
|
||||||
|
.product-carousel__nav--start {
|
||||||
|
inset-inline-start: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__nav--end {
|
||||||
|
inset-inline-end: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__track {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--carousel-gap);
|
||||||
|
overflow-x: auto;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__track::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__item {
|
||||||
|
/* 4 cards fill the remaining 4/5 of the row */
|
||||||
|
flex: 0 0
|
||||||
|
calc((100% - 3 * var(--carousel-gap)) / 4);
|
||||||
|
scroll-snap-align: start;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__state {
|
||||||
|
min-height: 280px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.product-carousel__row {
|
||||||
|
--carousel-slots: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__item {
|
||||||
|
flex-basis: calc((100% - 2 * var(--carousel-gap)) / 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.product-carousel {
|
||||||
|
margin-top: 64px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel--framed {
|
||||||
|
margin-top: 64px;
|
||||||
|
padding-block: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__row {
|
||||||
|
flex-direction: column;
|
||||||
|
--carousel-gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__aside {
|
||||||
|
flex-basis: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__item {
|
||||||
|
flex-basis: calc((100% - var(--carousel-gap)) / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-carousel__nav {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { useRef } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
||||||
|
import HeaderTitle from './HeaderTitle'
|
||||||
|
import ProductCard from './ProductCard'
|
||||||
|
import type { Product } from '../lib/types'
|
||||||
|
import './ProductCarousel.css'
|
||||||
|
|
||||||
|
type ProductCarouselProps = {
|
||||||
|
id?: string
|
||||||
|
placeholder: string
|
||||||
|
secondary: string
|
||||||
|
primary: string
|
||||||
|
products: Product[]
|
||||||
|
loading?: boolean
|
||||||
|
viewAllTo?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProductCarousel({
|
||||||
|
id,
|
||||||
|
placeholder,
|
||||||
|
secondary,
|
||||||
|
primary,
|
||||||
|
products,
|
||||||
|
loading = false,
|
||||||
|
viewAllTo = '/products',
|
||||||
|
className = '',
|
||||||
|
}: ProductCarouselProps) {
|
||||||
|
const scrollerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
function scrollByCard(direction: 1 | -1) {
|
||||||
|
const el = scrollerRef.current
|
||||||
|
if (!el) return
|
||||||
|
const card = el.querySelector<HTMLElement>('[data-carousel-card]')
|
||||||
|
const styles = getComputedStyle(el)
|
||||||
|
const gap = Number.parseFloat(styles.columnGap || styles.gap || '8') || 8
|
||||||
|
const amount = (card?.offsetWidth ?? 220) + gap
|
||||||
|
const rtl = styles.direction === 'rtl'
|
||||||
|
el.scrollBy({ left: amount * direction * (rtl ? -1 : 1), behavior: 'smooth' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!loading && products.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id={id} className={`product-carousel ${className}`.trim()}>
|
||||||
|
<div className="max-container">
|
||||||
|
{loading ? (
|
||||||
|
<div className="product-carousel__state" role="status">
|
||||||
|
در حال بارگذاری محصولات...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="product-carousel__row">
|
||||||
|
<aside className="product-carousel__aside">
|
||||||
|
<HeaderTitle
|
||||||
|
placeholder={placeholder}
|
||||||
|
secondary={secondary}
|
||||||
|
primary={primary}
|
||||||
|
size="lg"
|
||||||
|
align="start"
|
||||||
|
className="product-carousel__title"
|
||||||
|
/>
|
||||||
|
{viewAllTo && (
|
||||||
|
<Link to={viewAllTo} className="product-carousel__all">
|
||||||
|
مشاهده همه
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="product-carousel__stage">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="product-carousel__nav product-carousel__nav--start"
|
||||||
|
onClick={() => scrollByCard(-1)}
|
||||||
|
aria-label="قبلی"
|
||||||
|
>
|
||||||
|
<ChevronRight size={18} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div ref={scrollerRef} className="product-carousel__track">
|
||||||
|
{products.map((product) => (
|
||||||
|
<div key={product.id} data-carousel-card className="product-carousel__item">
|
||||||
|
<ProductCard product={product} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="product-carousel__nav product-carousel__nav--end"
|
||||||
|
onClick={() => scrollByCard(1)}
|
||||||
|
aria-label="بعدی"
|
||||||
|
>
|
||||||
|
<ChevronLeft size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -101,6 +101,7 @@ button {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
padding-bottom: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-container {
|
.max-container {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export type ListProductsParams = {
|
|||||||
q?: string
|
q?: string
|
||||||
categoryId?: string
|
categoryId?: string
|
||||||
categorySlug?: string
|
categorySlug?: string
|
||||||
|
tag?: string
|
||||||
minPrice?: number
|
minPrice?: number
|
||||||
maxPrice?: number
|
maxPrice?: number
|
||||||
page?: number
|
page?: number
|
||||||
@@ -112,6 +113,7 @@ function buildQuery(params: ListProductsParams) {
|
|||||||
if (params.q?.trim()) search.set('q', params.q.trim())
|
if (params.q?.trim()) search.set('q', params.q.trim())
|
||||||
if (params.categoryId) search.set('categoryId', params.categoryId)
|
if (params.categoryId) search.set('categoryId', params.categoryId)
|
||||||
if (params.categorySlug) search.set('categorySlug', params.categorySlug)
|
if (params.categorySlug) search.set('categorySlug', params.categorySlug)
|
||||||
|
if (params.tag?.trim()) search.set('tag', params.tag.trim())
|
||||||
if (params.minPrice !== undefined) search.set('minPrice', String(params.minPrice))
|
if (params.minPrice !== undefined) search.set('minPrice', String(params.minPrice))
|
||||||
if (params.maxPrice !== undefined) search.set('maxPrice', String(params.maxPrice))
|
if (params.maxPrice !== undefined) search.set('maxPrice', String(params.maxPrice))
|
||||||
if (params.page) search.set('page', String(params.page))
|
if (params.page) search.set('page', String(params.page))
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* Home product carousels driven by admin special tags (ویژه).
|
||||||
|
* Sort by `order`. English placeholders are fixed for the website.
|
||||||
|
* Carousels with zero products are not rendered.
|
||||||
|
*/
|
||||||
|
export type HomeCarouselDef = {
|
||||||
|
id: string
|
||||||
|
/** Matches Product.tags values set in the dashboard specials UI */
|
||||||
|
tag: string
|
||||||
|
order: number
|
||||||
|
placeholder: string
|
||||||
|
secondary: string
|
||||||
|
primary: string
|
||||||
|
/** Full-bleed band with top/bottom borders */
|
||||||
|
framed?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HOME_PRODUCT_CAROUSELS: HomeCarouselDef[] = [
|
||||||
|
{
|
||||||
|
id: 'recommended',
|
||||||
|
tag: 'پیشنهادی',
|
||||||
|
order: 1,
|
||||||
|
placeholder: 'Recommended',
|
||||||
|
secondary: 'محصولات',
|
||||||
|
primary: 'پیشنهادی مــــا',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bestsellers',
|
||||||
|
tag: 'پرفروش',
|
||||||
|
order: 2,
|
||||||
|
placeholder: 'Best Sellers',
|
||||||
|
secondary: 'محصولات',
|
||||||
|
primary: 'پرفــــــــروش',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ready-to-ship',
|
||||||
|
tag: 'آماده',
|
||||||
|
order: 3,
|
||||||
|
placeholder: 'Ready to Ship',
|
||||||
|
secondary: 'محصولات',
|
||||||
|
primary: 'آماده ارســــال',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'new-products',
|
||||||
|
tag: 'جدید',
|
||||||
|
order: 4,
|
||||||
|
placeholder: 'New Arrivals',
|
||||||
|
secondary: 'محصولات',
|
||||||
|
primary: 'جدیــــــــد',
|
||||||
|
framed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gift',
|
||||||
|
tag: 'هدیه',
|
||||||
|
order: 5,
|
||||||
|
placeholder: 'Gift Ideas',
|
||||||
|
secondary: 'محصولات',
|
||||||
|
primary: 'مناسب هدیــــه',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function sortedHomeCarousels(defs = HOME_PRODUCT_CAROUSELS) {
|
||||||
|
return [...defs].sort((a, b) => a.order - b.order)
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ export type Product = {
|
|||||||
intro?: string | null
|
intro?: string | null
|
||||||
description?: string | null
|
description?: string | null
|
||||||
tags?: string[]
|
tags?: string[]
|
||||||
|
preparationHours?: number | null
|
||||||
|
createdAt?: string
|
||||||
mainImageUrl: string | null
|
mainImageUrl: string | null
|
||||||
mainImageKey?: string | null
|
mainImageKey?: string | null
|
||||||
gallery?: ProductGalleryImage[]
|
gallery?: ProductGalleryImage[]
|
||||||
|
|||||||
+26
-18
@@ -165,14 +165,9 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
|
||||||
|
|
||||||
.sohan-section__top {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 28px;
|
||||||
margin-bottom: 20px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sohan-section__media {
|
.sohan-section__media {
|
||||||
@@ -207,10 +202,18 @@
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sohan-section__content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.sohan-section__title {
|
.sohan-section__title {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sohan-section__text {
|
.sohan-section__text {
|
||||||
@@ -218,8 +221,7 @@
|
|||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
color: var(--color-secondary);
|
color: var(--color-secondary);
|
||||||
position: relative;
|
margin: 0;
|
||||||
z-index: 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Offers wrapper */
|
/* Offers wrapper */
|
||||||
@@ -377,10 +379,11 @@
|
|||||||
margin-top: 120px;
|
margin-top: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sohan-section__top {
|
.sohan-section__inner {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 12px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sohan-section__media {
|
.sohan-section__media {
|
||||||
@@ -393,6 +396,11 @@
|
|||||||
width: 110%;
|
width: 110%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sohan-section__content {
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.sohan-section__title .header-title__placeholder {
|
.sohan-section__title .header-title__placeholder {
|
||||||
font-size: clamp(44px, 10vw, 72px);
|
font-size: clamp(44px, 10vw, 72px);
|
||||||
}
|
}
|
||||||
@@ -402,6 +410,10 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sohan-section__text {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.home-footer-block {
|
.home-footer-block {
|
||||||
margin-top: 200px;
|
margin-top: 200px;
|
||||||
}
|
}
|
||||||
@@ -466,11 +478,7 @@
|
|||||||
|
|
||||||
.sohan-section__inner {
|
.sohan-section__inner {
|
||||||
padding-inline: 0;
|
padding-inline: 0;
|
||||||
}
|
gap: 8px;
|
||||||
|
|
||||||
.sohan-section__top {
|
|
||||||
gap: 0;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sohan-section__media {
|
.sohan-section__media {
|
||||||
|
|||||||
+80
-2
@@ -2,6 +2,13 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ChevronLeft } from 'lucide-react'
|
import { ChevronLeft } from 'lucide-react'
|
||||||
import HeaderTitle from '../components/HeaderTitle'
|
import HeaderTitle from '../components/HeaderTitle'
|
||||||
|
import ProductCarousel from '../components/ProductCarousel'
|
||||||
|
import { listProducts } from '../lib/api'
|
||||||
|
import {
|
||||||
|
sortedHomeCarousels,
|
||||||
|
type HomeCarouselDef,
|
||||||
|
} from '../lib/homeCarousels'
|
||||||
|
import type { Product } from '../lib/types'
|
||||||
import macaronRight from '../assets/images/brown-macaron-right.png'
|
import macaronRight from '../assets/images/brown-macaron-right.png'
|
||||||
import macaronLeft from '../assets/images/brown-macaron-left.png'
|
import macaronLeft from '../assets/images/brown-macaron-left.png'
|
||||||
import chocolateCake from '../assets/images/chocolate-cake.png'
|
import chocolateCake from '../assets/images/chocolate-cake.png'
|
||||||
@@ -16,6 +23,13 @@ import flatLay from '../assets/images/FlatLay.png'
|
|||||||
import peste from '../assets/images/peste.png'
|
import peste from '../assets/images/peste.png'
|
||||||
import './HomePage.css'
|
import './HomePage.css'
|
||||||
|
|
||||||
|
const CAROUSEL_LIMIT = 12
|
||||||
|
|
||||||
|
/** Fixed placement slots between homepage frames; filled by order. */
|
||||||
|
const SLOT_COUNT = 4
|
||||||
|
|
||||||
|
type CarouselSection = HomeCarouselDef & { products: Product[] }
|
||||||
|
|
||||||
const DECOR_CARDS = [
|
const DECOR_CARDS = [
|
||||||
{
|
{
|
||||||
title: 'انواع',
|
title: 'انواع',
|
||||||
@@ -58,11 +72,66 @@ const OFFER_CATEGORIES = [
|
|||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [openOffer, setOpenOffer] = useState(0)
|
const [openOffer, setOpenOffer] = useState(0)
|
||||||
|
const [carousels, setCarousels] = useState<CarouselSection[]>([])
|
||||||
|
const [carouselsLoading, setCarouselsLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'شیرینی بلوط - صفحه اصلی'
|
document.title = 'شیرینی بلوط - صفحه اصلی'
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setCarouselsLoading(true)
|
||||||
|
|
||||||
|
const defs = sortedHomeCarousels()
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
defs.map(async (def) => {
|
||||||
|
try {
|
||||||
|
const result = await listProducts({
|
||||||
|
tag: def.tag,
|
||||||
|
pageSize: CAROUSEL_LIMIT,
|
||||||
|
})
|
||||||
|
return { ...def, products: result.items }
|
||||||
|
} catch {
|
||||||
|
return { ...def, products: [] as Product[] }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.then((results) => {
|
||||||
|
if (cancelled) return
|
||||||
|
setCarousels(results.filter((item) => item.products.length > 0))
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) setCarouselsLoading(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const slotted = carousels.slice(0, SLOT_COUNT)
|
||||||
|
const overflow = carousels.slice(SLOT_COUNT)
|
||||||
|
|
||||||
|
function renderCarousel(section: CarouselSection | undefined) {
|
||||||
|
if (!section) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProductCarousel
|
||||||
|
key={section.id}
|
||||||
|
id={section.id}
|
||||||
|
placeholder={section.placeholder}
|
||||||
|
secondary={section.secondary}
|
||||||
|
primary={section.primary}
|
||||||
|
products={section.products}
|
||||||
|
loading={carouselsLoading}
|
||||||
|
viewAllTo="/products"
|
||||||
|
className={section.framed ? 'product-carousel--framed' : undefined}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="home">
|
<div className="home">
|
||||||
{/* Hero title */}
|
{/* Hero title */}
|
||||||
@@ -89,6 +158,8 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{renderCarousel(slotted[0])}
|
||||||
|
|
||||||
{/* For The Best */}
|
{/* For The Best */}
|
||||||
<HeaderTitle
|
<HeaderTitle
|
||||||
placeholder="For The Best"
|
placeholder="For The Best"
|
||||||
@@ -130,6 +201,8 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{renderCarousel(slotted[1])}
|
||||||
|
|
||||||
{/* About */}
|
{/* About */}
|
||||||
<div className="max-container about-section">
|
<div className="max-container about-section">
|
||||||
<div className="about-section__image">
|
<div className="about-section__image">
|
||||||
@@ -152,15 +225,17 @@ export default function HomePage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{renderCarousel(slotted[2])}
|
||||||
|
|
||||||
{/* Sohan */}
|
{/* Sohan */}
|
||||||
<section className="sohan-section">
|
<section className="sohan-section">
|
||||||
<img src={peste} alt="" className="sohan-section__peste" />
|
<img src={peste} alt="" className="sohan-section__peste" />
|
||||||
<div className="sohan-section__inner">
|
<div className="sohan-section__inner">
|
||||||
<div className="sohan-section__top">
|
|
||||||
<div className="sohan-section__media">
|
<div className="sohan-section__media">
|
||||||
<img src={sohan} alt="سوهان" className="sohan-section__image" />
|
<img src={sohan} alt="سوهان" className="sohan-section__image" />
|
||||||
<img src={sohanPattern} alt="" className="sohan-section__pattern" />
|
<img src={sohanPattern} alt="" className="sohan-section__pattern" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="sohan-section__content">
|
||||||
<HeaderTitle
|
<HeaderTitle
|
||||||
placeholder="Sohan"
|
placeholder="Sohan"
|
||||||
secondary="سوهان"
|
secondary="سوهان"
|
||||||
@@ -169,15 +244,18 @@ export default function HomePage() {
|
|||||||
align="start"
|
align="start"
|
||||||
className="sohan-section__title"
|
className="sohan-section__title"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<p className="sohan-section__text">
|
<p className="sohan-section__text">
|
||||||
سوهان یکی از اصیلترین سوغات قم است که در شیرینیسرای بلوط با استفاده از مواد اولیه
|
سوهان یکی از اصیلترین سوغات قم است که در شیرینیسرای بلوط با استفاده از مواد اولیه
|
||||||
باکیفیت و تازه تهیه میشود. عطر دلانگیز کره و زعفران در کنار بافتی لطیف و طعمی
|
باکیفیت و تازه تهیه میشود. عطر دلانگیز کره و زعفران در کنار بافتی لطیف و طعمی
|
||||||
ماندگار، سوهان بلوط را به انتخابی مناسب برای پذیرایی و هدیه تبدیل کرده است.
|
ماندگار، سوهان بلوط را به انتخابی مناسب برای پذیرایی و هدیه تبدیل کرده است.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{renderCarousel(slotted[3])}
|
||||||
|
{overflow.map((section) => renderCarousel(section))}
|
||||||
|
|
||||||
{/* Special offers + hours */}
|
{/* Special offers + hours */}
|
||||||
<div className="home-footer-block">
|
<div className="home-footer-block">
|
||||||
<section className="offers-wrapper">
|
<section className="offers-wrapper">
|
||||||
|
|||||||
@@ -69,6 +69,10 @@
|
|||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-images--single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.product-images__main img,
|
.product-images__main img,
|
||||||
.product-images__thumbs img {
|
.product-images__thumbs img {
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
@@ -78,6 +82,34 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-images__main-btn {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: zoom-in;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-images__gallery-hint {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.product-images__thumbs {
|
.product-images__thumbs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -92,10 +124,12 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s ease, opacity 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-images__thumbs button.is-active {
|
.product-images__thumbs button:hover {
|
||||||
border-color: var(--color-primary);
|
border-color: rgba(143, 65, 12, 0.35);
|
||||||
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product--name {
|
.product--name {
|
||||||
@@ -152,6 +186,47 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-detail__options {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-detail__option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(143, 65, 12, 0.12);
|
||||||
|
background: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-detail__option-name {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
font-size: clamp(12px, 3vw, 15px);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-detail__option-price {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: clamp(11px, 2.8vw, 14px);
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-detail__options-hint {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: var(--color-gray-400);
|
||||||
|
font-size: clamp(11px, 2.6vw, 13px);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.product-detail__cart-wrap {
|
.product-detail__cart-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
+103
-27
@@ -1,14 +1,14 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { Link, useParams } from 'react-router-dom'
|
import { Link, useParams } from 'react-router-dom'
|
||||||
import { ChevronLeft, ChevronRight, Minus, Plus, Star } from 'lucide-react'
|
import { ChevronLeft, ChevronRight, Images, Minus, Plus, Star } from 'lucide-react'
|
||||||
import PageHeader from '../components/PageHeader'
|
import PageHeader from '../components/PageHeader'
|
||||||
import ProductCard from '../components/ProductCard'
|
import ProductCard from '../components/ProductCard'
|
||||||
import ProductOptionsModal from '../components/ProductOptionsModal'
|
import ProductOptionsModal from '../components/ProductOptionsModal'
|
||||||
|
import ImageLightbox from '../components/ImageLightbox'
|
||||||
import { getProduct, listProducts } from '../lib/api'
|
import { getProduct, listProducts } from '../lib/api'
|
||||||
import { addToCart } from '../lib/cart'
|
import { addToCart } from '../lib/cart'
|
||||||
import type { Product, ProductOptionValue } from '../lib/types'
|
import type { Product, ProductOptionValue } from '../lib/types'
|
||||||
import { formatProductPrice, productImageSrc } from '../lib/types'
|
import { formatProductPrice, formatPriceFa, productImageSrc } from '../lib/types'
|
||||||
import handbagIcon from '../assets/images/Handbag.svg'
|
|
||||||
import cakeIcon from '../assets/images/Cake.svg'
|
import cakeIcon from '../assets/images/Cake.svg'
|
||||||
import defaultPhoto from '../assets/images/DefaultPhoto.png'
|
import defaultPhoto from '../assets/images/DefaultPhoto.png'
|
||||||
import './ProductDetailPage.css'
|
import './ProductDetailPage.css'
|
||||||
@@ -21,16 +21,34 @@ function stripHtml(value: string) {
|
|||||||
.trim()
|
.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionPreviewLabel(option: ProductOptionValue) {
|
||||||
|
const flavor = option.flavor?.nameFa?.trim()
|
||||||
|
if (flavor && option.amount) return `${flavor} — ${option.amount}`
|
||||||
|
return flavor || option.amount || 'گزینه'
|
||||||
|
}
|
||||||
|
|
||||||
|
function uniqueUrls(urls: Array<string | null | undefined>) {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
const result: string[] = []
|
||||||
|
for (const url of urls) {
|
||||||
|
if (!url || seen.has(url)) continue
|
||||||
|
seen.add(url)
|
||||||
|
result.push(url)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProductDetailPage() {
|
export default function ProductDetailPage() {
|
||||||
const { slugOrId = '', id = '' } = useParams()
|
const { slugOrId = '', id = '' } = useParams()
|
||||||
const productId = id || slugOrId
|
const productId = id || slugOrId
|
||||||
const [product, setProduct] = useState<Product | null>(null)
|
const [product, setProduct] = useState<Product | null>(null)
|
||||||
const [related, setRelated] = useState<Product[]>([])
|
const [related, setRelated] = useState<Product[]>([])
|
||||||
const [activeImage, setActiveImage] = useState<string>('')
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [quantity, setQuantity] = useState(1)
|
const [quantity, setQuantity] = useState(1)
|
||||||
const [optionsOpen, setOptionsOpen] = useState(false)
|
const [optionsOpen, setOptionsOpen] = useState(false)
|
||||||
|
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||||
|
const [lightboxIndex, setLightboxIndex] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'شیرینی بلوط - صفحه جزئیات محصول'
|
document.title = 'شیرینی بلوط - صفحه جزئیات محصول'
|
||||||
@@ -43,16 +61,12 @@ export default function ProductDetailPage() {
|
|||||||
setError(null)
|
setError(null)
|
||||||
setProduct(null)
|
setProduct(null)
|
||||||
setRelated([])
|
setRelated([])
|
||||||
|
setLightboxOpen(false)
|
||||||
|
|
||||||
getProduct(productId)
|
getProduct(productId)
|
||||||
.then((item) => {
|
.then((item) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setProduct(item)
|
setProduct(item)
|
||||||
const galleryUrls = [
|
|
||||||
item.mainImageUrl,
|
|
||||||
...(item.gallery ?? []).map((g) => g.url),
|
|
||||||
].filter(Boolean) as string[]
|
|
||||||
setActiveImage(galleryUrls[0] || defaultPhoto)
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
listProducts({
|
listProducts({
|
||||||
@@ -79,18 +93,21 @@ export default function ProductDetailPage() {
|
|||||||
}
|
}
|
||||||
}, [productId])
|
}, [productId])
|
||||||
|
|
||||||
const thumbs = useMemo(() => {
|
const mainImage = product ? productImageSrc(product, defaultPhoto) : defaultPhoto
|
||||||
|
|
||||||
|
const galleryImages = useMemo(() => {
|
||||||
if (!product) return []
|
if (!product) return []
|
||||||
const urls = [
|
const main = product.mainImageUrl || ''
|
||||||
product.mainImageUrl,
|
return uniqueUrls((product.gallery ?? []).map((g) => g.url)).filter((url) => url !== main)
|
||||||
...(product.gallery ?? []).map((g) => g.url),
|
|
||||||
].filter(Boolean) as string[]
|
|
||||||
if (urls.length === 0) return [defaultPhoto]
|
|
||||||
const unique = [...new Set(urls)]
|
|
||||||
if (unique.length === 1) return [unique[0], unique[0]]
|
|
||||||
return unique.slice(0, 6)
|
|
||||||
}, [product])
|
}, [product])
|
||||||
|
|
||||||
|
const lightboxImages = useMemo(
|
||||||
|
() => uniqueUrls([mainImage, ...galleryImages]),
|
||||||
|
[mainImage, galleryImages],
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasGallery = galleryImages.length > 0
|
||||||
|
|
||||||
const price = product ? formatProductPrice(product) : null
|
const price = product ? formatProductPrice(product) : null
|
||||||
const abstractText = product?.intro ? stripHtml(product.intro) : ''
|
const abstractText = product?.intro ? stripHtml(product.intro) : ''
|
||||||
const descriptionHtml = product?.description?.trim() || ''
|
const descriptionHtml = product?.description?.trim() || ''
|
||||||
@@ -120,6 +137,12 @@ export default function ProductDetailPage() {
|
|||||||
commitAdd()
|
commitAdd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openLightbox(index = 0) {
|
||||||
|
if (lightboxImages.length === 0) return
|
||||||
|
setLightboxIndex(index)
|
||||||
|
setLightboxOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuantity(product?.sellUnit === 'kilo' ? 0.5 : 1)
|
setQuantity(product?.sellUnit === 'kilo' ? 0.5 : 1)
|
||||||
setOptionsOpen(false)
|
setOptionsOpen(false)
|
||||||
@@ -135,7 +158,7 @@ export default function ProductDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<PageHeader
|
<PageHeader
|
||||||
icon={handbagIcon}
|
icon={cakeIcon}
|
||||||
placeholder="Product Details"
|
placeholder="Product Details"
|
||||||
secondary="جزئیات"
|
secondary="جزئیات"
|
||||||
primary="محصــــول ما"
|
primary="محصــــول ما"
|
||||||
@@ -166,22 +189,42 @@ export default function ProductDetailPage() {
|
|||||||
{!loading && product && price && (
|
{!loading && product && price && (
|
||||||
<>
|
<>
|
||||||
<div className="product-detail__grid">
|
<div className="product-detail__grid">
|
||||||
<div className="product-images">
|
<div
|
||||||
|
className={`product-images${hasGallery ? '' : ' product-images--single'}`}
|
||||||
|
>
|
||||||
<div className="product-images__main">
|
<div className="product-images__main">
|
||||||
<img src={activeImage || productImageSrc(product, defaultPhoto)} alt={product.nameFa} />
|
|
||||||
</div>
|
|
||||||
<div className="product-images__thumbs">
|
|
||||||
{thumbs.map((url, index) => (
|
|
||||||
<button
|
<button
|
||||||
key={`${url}-${index}`}
|
|
||||||
type="button"
|
type="button"
|
||||||
className={activeImage === url ? 'is-active' : undefined}
|
className="product-images__main-btn"
|
||||||
onClick={() => setActiveImage(url)}
|
onClick={() => openLightbox(0)}
|
||||||
|
aria-label={
|
||||||
|
hasGallery ? 'باز کردن گالری تصاویر' : 'مشاهده تصویر بزرگتر'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img src={mainImage} alt={product.nameFa} />
|
||||||
|
{hasGallery && (
|
||||||
|
<span className="product-images__gallery-hint">
|
||||||
|
<Images size={16} />
|
||||||
|
گالری ({galleryImages.length})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasGallery && (
|
||||||
|
<div className="product-images__thumbs">
|
||||||
|
{galleryImages.map((url, index) => (
|
||||||
|
<button
|
||||||
|
key={url}
|
||||||
|
type="button"
|
||||||
|
onClick={() => openLightbox(index + 1)}
|
||||||
|
aria-label={`تصویر گالری ${index + 1}`}
|
||||||
>
|
>
|
||||||
<img src={url} alt="" />
|
<img src={url} alt="" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="product-detail__info">
|
<div className="product-detail__info">
|
||||||
@@ -215,6 +258,29 @@ export default function ProductDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(product.options?.length ?? 0) > 0 && (
|
||||||
|
<div className="product-detail__block">
|
||||||
|
<div className="caption__title">گزینهها :</div>
|
||||||
|
<ul className="product-detail__options" aria-label="گزینههای محصول">
|
||||||
|
{product.options!.map((option) => (
|
||||||
|
<li key={option.id} className="product-detail__option">
|
||||||
|
<span className="product-detail__option-name">
|
||||||
|
{optionPreviewLabel(option)}
|
||||||
|
</span>
|
||||||
|
{option.price > 0 && (
|
||||||
|
<span className="product-detail__option-price">
|
||||||
|
+{formatPriceFa(option.price)} تومان
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<p className="product-detail__options-hint">
|
||||||
|
انتخاب گزینه هنگام افزودن به سبد خرید انجام میشود.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="product-detail__cart-wrap">
|
<div className="product-detail__cart-wrap">
|
||||||
<div className="qty-stepper" dir="ltr">
|
<div className="qty-stepper" dir="ltr">
|
||||||
<button
|
<button
|
||||||
@@ -269,6 +335,16 @@ export default function ProductDetailPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{lightboxOpen && lightboxImages.length > 0 && (
|
||||||
|
<ImageLightbox
|
||||||
|
images={lightboxImages}
|
||||||
|
index={lightboxIndex}
|
||||||
|
alt={product.nameFa}
|
||||||
|
onClose={() => setLightboxOpen(false)}
|
||||||
|
onIndexChange={setLightboxIndex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{descriptionHtml && (
|
{descriptionHtml && (
|
||||||
<section className="product-detail__description">
|
<section className="product-detail__description">
|
||||||
<div className="caption__title">توضیحات محصول :</div>
|
<div className="caption__title">توضیحات محصول :</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user