mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { ImageLightbox } from '../components/ImageLightbox'
|
||||
import { ProductDetailsTabs } from '../components/ProductDetailsTabs'
|
||||
import { ApiError } from '../lib/api'
|
||||
import {
|
||||
getProduct,
|
||||
mapProductApiToUi,
|
||||
} from '../services/productService'
|
||||
import type { Product } from '../types/product'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import styles from './ProductDetailsPage.module.css'
|
||||
|
||||
export function ProductDetailsPage() {
|
||||
const { id } = useParams()
|
||||
const [product, setProduct] = useState<Product | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState('')
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const [lightboxOpen, setLightboxOpen] = useState(false)
|
||||
const [lightboxIndex, setLightboxIndex] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return
|
||||
|
||||
const controller = new AbortController()
|
||||
|
||||
async function loadProduct() {
|
||||
setIsLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const data = await getProduct(id!, controller.signal)
|
||||
setProduct(mapProductApiToUi(data))
|
||||
setActiveIndex(0)
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Unable to load product.')
|
||||
}
|
||||
setProduct(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void loadProduct()
|
||||
return () => controller.abort()
|
||||
}, [id])
|
||||
|
||||
const galleryImages = useMemo(() => product?.images ?? [], [product])
|
||||
const currentImage = galleryImages[activeIndex] ?? galleryImages[0] ?? ''
|
||||
|
||||
function openLightbox(index: number) {
|
||||
setLightboxIndex(index)
|
||||
setLightboxOpen(true)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<p className={styles.status}>Loading product...</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !product) {
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<p className={styles.error}>{error || 'Product not found.'}</p>
|
||||
<Link to="/products/list" className={styles.backLink}>
|
||||
Back to My Products
|
||||
</Link>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Products', href: '/products' },
|
||||
{ label: 'My Products', href: '/products/list' },
|
||||
{ label: product.nameEn },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className={styles.layout}>
|
||||
<div className={styles.gallery}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.mainImage}
|
||||
onClick={() => currentImage && openLightbox(activeIndex)}
|
||||
aria-label="Open image gallery"
|
||||
disabled={!currentImage}
|
||||
>
|
||||
{currentImage ? (
|
||||
<img src={currentImage} alt={product.nameEn} />
|
||||
) : (
|
||||
<div className={styles.imagePlaceholder}>No image</div>
|
||||
)}
|
||||
{product.status === 'draft' && <span className={styles.draftBadge}>Draft</span>}
|
||||
</button>
|
||||
|
||||
{galleryImages.length > 1 && (
|
||||
<div className={styles.thumbnails}>
|
||||
{galleryImages.map((src, index) => (
|
||||
<button
|
||||
key={`${src}-${index}`}
|
||||
type="button"
|
||||
className={`${styles.thumb} ${index === activeIndex ? styles.thumbActive : ''}`}
|
||||
onClick={() => {
|
||||
setActiveIndex(index)
|
||||
openLightbox(index)
|
||||
}}
|
||||
aria-label={`View image ${index + 1}`}
|
||||
>
|
||||
<img src={src} alt="" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
{product.category && <span className={styles.categoryChip}>{product.category}</span>}
|
||||
|
||||
<h1 className={styles.nameEn}>{product.nameEn}</h1>
|
||||
{product.nameFa && <p className={styles.nameFa}>{product.nameFa}</p>}
|
||||
|
||||
{product.summary && <p className={styles.summary}>{product.summary}</p>}
|
||||
|
||||
{product.description && (
|
||||
<div
|
||||
className={styles.description}
|
||||
dangerouslySetInnerHTML={{ __html: product.description }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{product.tags.length > 0 && (
|
||||
<div className={styles.tags}>
|
||||
{product.tags.map((tag) => (
|
||||
<span key={tag} className={styles.tag}>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProductDetailsTabs productId={product.id} commentCount={product.commentCount} />
|
||||
|
||||
<ImageLightbox
|
||||
open={lightboxOpen}
|
||||
images={galleryImages}
|
||||
initialIndex={lightboxIndex}
|
||||
alt={product.nameEn}
|
||||
onClose={() => setLightboxOpen(false)}
|
||||
/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user