Add customer/manager access UI and tighten customers filters.
Business and super-admin can set Customer vs Manager roles (Admin only for super-admin), and the customers page drops clutter while filtering by all/customers/managers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
404c008ced
commit
f622e6d605
@@ -1,8 +1,10 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { ImageLightbox } from '../components/ImageLightbox'
|
||||
import { ProductDetailsTabs } from '../components/ProductDetailsTabs'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError } from '../lib/api'
|
||||
import {
|
||||
getProduct,
|
||||
@@ -14,6 +16,9 @@ import pageStyles from '../components/PageContent.module.css'
|
||||
import styles from './ProductDetailsPage.module.css'
|
||||
|
||||
export function ProductDetailsPage() {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const isFa = locale === 'fa'
|
||||
const { id } = useParams()
|
||||
const [product, setProduct] = useState<Product | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
@@ -40,7 +45,7 @@ export function ProductDetailsPage() {
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError('Unable to load product.')
|
||||
setError(t('products.details.errorLoad'))
|
||||
}
|
||||
setProduct(null)
|
||||
} finally {
|
||||
@@ -50,7 +55,7 @@ export function ProductDetailsPage() {
|
||||
|
||||
void loadProduct()
|
||||
return () => controller.abort()
|
||||
}, [id])
|
||||
}, [id, t])
|
||||
|
||||
const galleryImages = useMemo(() => product?.images ?? [], [product])
|
||||
const currentImage = galleryImages[activeIndex] ?? galleryImages[0] ?? ''
|
||||
@@ -63,7 +68,7 @@ export function ProductDetailsPage() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<p className={styles.status}>Loading product...</p>
|
||||
<p className={styles.status}>{t('products.details.loading')}</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -71,16 +76,27 @@ export function ProductDetailsPage() {
|
||||
if (error || !product) {
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<p className={styles.error}>{error || 'Product not found.'}</p>
|
||||
<p className={styles.error}>{error || t('products.details.notFound')}</p>
|
||||
<Link to="/products/list" className={styles.backLink}>
|
||||
Back to My Products
|
||||
{t('products.details.back')}
|
||||
</Link>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const nameEnLocale = textLocaleAttrs(product.nameEn)
|
||||
const nameFaLocale = textLocaleAttrs(product.nameFa)
|
||||
const primaryName = isFa
|
||||
? product.nameFa?.trim() || product.nameEn
|
||||
: product.nameEn?.trim() || product.nameFa
|
||||
const secondaryName = isFa
|
||||
? product.nameEn?.trim() && product.nameEn !== primaryName
|
||||
? product.nameEn
|
||||
: ''
|
||||
: product.nameFa?.trim() && product.nameFa !== primaryName
|
||||
? product.nameFa
|
||||
: ''
|
||||
|
||||
const primaryLocale = textLocaleAttrs(primaryName)
|
||||
const secondaryLocale = textLocaleAttrs(secondaryName)
|
||||
const summaryLocale = textLocaleAttrs(product.summary)
|
||||
const descriptionLocale = textLocaleAttrs(
|
||||
product.description?.replace(/<[^>]+>/g, ' ') ?? '',
|
||||
@@ -94,7 +110,7 @@ export function ProductDetailsPage() {
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Products', href: '/products' },
|
||||
{ label: 'My Products', href: '/products/list' },
|
||||
{ label: product.nameEn },
|
||||
{ label: primaryName },
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -104,15 +120,17 @@ export function ProductDetailsPage() {
|
||||
type="button"
|
||||
className={styles.mainImage}
|
||||
onClick={() => currentImage && openLightbox(activeIndex)}
|
||||
aria-label="Open image gallery"
|
||||
aria-label={t('products.details.openGallery')}
|
||||
disabled={!currentImage}
|
||||
>
|
||||
{currentImage ? (
|
||||
<img src={currentImage} alt={product.nameEn} />
|
||||
<img src={currentImage} alt={primaryName} />
|
||||
) : (
|
||||
<div className={styles.imagePlaceholder}>No image</div>
|
||||
<div className={styles.imagePlaceholder}>{t('products.details.noImage')}</div>
|
||||
)}
|
||||
{product.status === 'draft' && (
|
||||
<span className={styles.draftBadge}>{t('products.card.draft')}</span>
|
||||
)}
|
||||
{product.status === 'draft' && <span className={styles.draftBadge}>Draft</span>}
|
||||
</button>
|
||||
|
||||
{galleryImages.length > 1 && (
|
||||
@@ -126,7 +144,7 @@ export function ProductDetailsPage() {
|
||||
setActiveIndex(index)
|
||||
openLightbox(index)
|
||||
}}
|
||||
aria-label={`View image ${index + 1}`}
|
||||
aria-label={t('products.details.viewImage', { index: index + 1 })}
|
||||
>
|
||||
<img src={src} alt="" />
|
||||
</button>
|
||||
@@ -147,21 +165,21 @@ export function ProductDetailsPage() {
|
||||
)}
|
||||
|
||||
<h1
|
||||
className={[styles.nameEn, nameEnLocale.className].filter(Boolean).join(' ')}
|
||||
lang={nameEnLocale.lang}
|
||||
dir={nameEnLocale.dir}
|
||||
className={[styles.nameEn, primaryLocale.className].filter(Boolean).join(' ')}
|
||||
lang={primaryLocale.lang}
|
||||
dir={primaryLocale.dir}
|
||||
>
|
||||
{product.nameEn}
|
||||
{primaryName}
|
||||
</h1>
|
||||
{product.nameFa && (
|
||||
{secondaryName ? (
|
||||
<p
|
||||
className={[styles.nameFa, nameFaLocale.className].filter(Boolean).join(' ')}
|
||||
lang={nameFaLocale.lang}
|
||||
dir={nameFaLocale.dir}
|
||||
className={[styles.nameFa, secondaryLocale.className].filter(Boolean).join(' ')}
|
||||
lang={secondaryLocale.lang}
|
||||
dir={secondaryLocale.dir}
|
||||
>
|
||||
{product.nameFa}
|
||||
{secondaryName}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{product.summary && (
|
||||
<p
|
||||
@@ -210,7 +228,7 @@ export function ProductDetailsPage() {
|
||||
open={lightboxOpen}
|
||||
images={galleryImages}
|
||||
initialIndex={lightboxIndex}
|
||||
alt={product.nameEn}
|
||||
alt={primaryName}
|
||||
onClose={() => setLightboxOpen(false)}
|
||||
/>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user