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,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Plus } from 'lucide-react'
|
||||
import { FileText, Plus } from 'lucide-react'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { BlogCard } from '../components/BlogCard'
|
||||
import { BlogCommentsModal } from '../components/BlogCommentsModal'
|
||||
@@ -180,7 +180,21 @@ export function BlogListPage() {
|
||||
{isLoading ? (
|
||||
<p className={styles.empty}>{t('blog.list.loading')}</p>
|
||||
) : blogs.length === 0 ? (
|
||||
<p className={styles.empty}>{t('blog.list.empty')}</p>
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyIcon} aria-hidden="true">
|
||||
<FileText size={28} strokeWidth={1.75} />
|
||||
</div>
|
||||
<h3 className={styles.emptyTitle}>{t('blog.list.empty')}</h3>
|
||||
<p className={styles.emptyHint}>{t('blog.list.emptyHint')}</p>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.emptyCta}
|
||||
onClick={() => navigate('/blog/new')}
|
||||
>
|
||||
<Plus size={18} strokeWidth={2.25} />
|
||||
{t('blog.list.emptyCta')}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={pageStyles.grid}>
|
||||
|
||||
@@ -8,6 +8,82 @@
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 56px 28px;
|
||||
text-align: center;
|
||||
background: var(--glass-bg);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--glass-shadow);
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 6px;
|
||||
border-radius: 16px;
|
||||
color: var(--primary);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 255, 255, 0.72) 0%,
|
||||
rgba(var(--primary-rgb) / 0.12) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.45);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||
}
|
||||
|
||||
.emptyTitle {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.emptyHint {
|
||||
margin: 0 0 8px;
|
||||
max-width: 360px;
|
||||
font-size: 13px;
|
||||
line-height: 1.65;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.emptyCta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 42px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 8px 20px rgba(var(--primary-rgb) / 0.32);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
button.emptyCta {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyCta:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 26px rgba(var(--primary-rgb) / 0.4);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
|
||||
@@ -126,12 +126,14 @@
|
||||
}
|
||||
|
||||
.badge[data-status='draft'] {
|
||||
color: #fff;
|
||||
color: #0f172a;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
rgba(251, 191, 36, 0.55) 0%,
|
||||
rgba(245, 158, 11, 0.32) 100%
|
||||
rgba(255, 255, 255, 0.78) 0%,
|
||||
rgba(255, 255, 255, 0.42) 100%
|
||||
);
|
||||
border-color: rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
.badge[data-status='published'] {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.pageHeaderCompact {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pageHeaderCompact :global(h2) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tablePanel {
|
||||
margin-top: 12px;
|
||||
padding: 0;
|
||||
@@ -61,7 +69,7 @@
|
||||
}
|
||||
|
||||
.colActions {
|
||||
width: 206px;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.th,
|
||||
@@ -132,10 +140,9 @@
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
/* FA: عملیات column is on the physical left — pin controls to that edge. */
|
||||
/* FA: عملیات column is on the physical left. */
|
||||
.tableFa .thActions,
|
||||
.tableFa .tdActions {
|
||||
text-align: left;
|
||||
@@ -143,12 +150,8 @@
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.tableFa .rowActions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.toggleInActions {
|
||||
margin-right: 10px;
|
||||
margin-inline-end: 10px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,28 +1,33 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { MessageSquare, Pencil, Plus, RotateCcw, Search, Ticket, Trash2 } from 'lucide-react'
|
||||
import { MessageSquare, Pencil, Plus, RotateCcw, Search, Shield, Ticket, Trash2 } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { AddCustomerModal } from '../components/AddCustomerModal'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { ChangeUserAccessModal } from '../components/ChangeUserAccessModal'
|
||||
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
|
||||
import { EditCustomerModal } from '../components/EditCustomerModal'
|
||||
import { Pagination } from '../components/Pagination'
|
||||
import { ToggleSwitch } from '../components/ToggleSwitch'
|
||||
import { Tooltip } from '../components/Tooltip'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError, isAbortError } from '../lib/api'
|
||||
import { getActiveBusinessId } from '../lib/businessContext'
|
||||
import { formatCellForDisplay } from '../lib/cellNumber'
|
||||
import {
|
||||
listCustomers,
|
||||
removeCustomer,
|
||||
updateCustomerEnabled,
|
||||
type BusinessCustomerListItem,
|
||||
type CustomerAccessFilter,
|
||||
type CustomersListResponse,
|
||||
} from '../services/customerService'
|
||||
import { formatIrtPrice } from '../utils/irtPrice'
|
||||
import { textLocaleAttrs } from '../utils/textLocale'
|
||||
import filterStyles from '../components/ListFiltersPanel.module.css'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import accessStyles from '../components/ChangeUserAccessModal.module.css'
|
||||
import styles from './CustomersPage.module.css'
|
||||
|
||||
const PAGE_SIZE = 24
|
||||
@@ -47,26 +52,66 @@ function formatTransactionTotal(total: number | null | undefined) {
|
||||
return formatIrtPrice(total)
|
||||
}
|
||||
|
||||
function accessBadgeKey(user: BusinessCustomerListItem): string | null {
|
||||
if (user.isBusinessOwner) return 'customers.access.badge.owner'
|
||||
if (user.teamRole === 'admin') return 'customers.access.badge.admin'
|
||||
if (user.teamRole === 'editor') return 'customers.access.badge.editor'
|
||||
if (user.teamRole === 'viewer') return 'customers.access.badge.viewer'
|
||||
if (user.businessMemberId) return 'customers.access.badge.manager'
|
||||
return null
|
||||
}
|
||||
|
||||
export function CustomersPage() {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const { user: authUser } = useAuth()
|
||||
const { showToast } = useToast()
|
||||
const [data, setData] = useState<CustomersListResponse | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const [appliedFilters, setAppliedFilters] = useState<{ name?: string; cellNumber?: string }>({})
|
||||
const [appliedFilters, setAppliedFilters] = useState<{
|
||||
name?: string
|
||||
cellNumber?: string
|
||||
access: CustomerAccessFilter
|
||||
}>({ access: 'all' })
|
||||
const [page, setPage] = useState(1)
|
||||
|
||||
const [draftName, setDraftName] = useState('')
|
||||
const [draftCell, setDraftCell] = useState('')
|
||||
const [draftAccess, setDraftAccess] = useState<CustomerAccessFilter>('all')
|
||||
|
||||
const [togglingId, setTogglingId] = useState<string | null>(null)
|
||||
const [editTarget, setEditTarget] = useState<BusinessCustomerListItem | null>(null)
|
||||
const [accessTarget, setAccessTarget] = useState<BusinessCustomerListItem | null>(null)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [removeTarget, setRemoveTarget] = useState<BusinessCustomerListItem | null>(null)
|
||||
const [removing, setRemoving] = useState(false)
|
||||
|
||||
const membership = useMemo(() => {
|
||||
const activeId = getActiveBusinessId()
|
||||
if (!authUser?.businesses.length) return undefined
|
||||
return (
|
||||
authUser.businesses.find((b) => String(b.id) === String(activeId)) ??
|
||||
authUser.businesses[0]
|
||||
)
|
||||
}, [authUser])
|
||||
const isSuperAdmin = Boolean(
|
||||
authUser?.isSuperAdmin || authUser?.roles.includes('super_admin'),
|
||||
)
|
||||
const canManageAccess =
|
||||
isSuperAdmin ||
|
||||
Boolean(membership?.isOwner) ||
|
||||
Boolean(membership?.permissions.includes('business.team.update'))
|
||||
|
||||
function accessChangeDisabledReason(customer: BusinessCustomerListItem): string | null {
|
||||
if (customer.isBusinessOwner) return t('customers.access.ownerLocked')
|
||||
if (customer.teamRole === 'admin' && !isSuperAdmin) {
|
||||
return t('customers.access.adminLocked')
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
@@ -97,7 +142,7 @@ export function CustomersPage() {
|
||||
return () => {
|
||||
controller.abort()
|
||||
}
|
||||
}, [page, appliedFilters.name, appliedFilters.cellNumber, t])
|
||||
}, [page, appliedFilters.name, appliedFilters.cellNumber, appliedFilters.access, t])
|
||||
|
||||
const totalPages = useMemo(() => {
|
||||
const total = data?.total ?? 0
|
||||
@@ -117,6 +162,7 @@ export function CustomersPage() {
|
||||
function applyFilters() {
|
||||
setPage(1)
|
||||
setAppliedFilters({
|
||||
access: draftAccess,
|
||||
...(draftName.trim() ? { name: draftName.trim() } : {}),
|
||||
...(draftCell.trim() ? { cellNumber: draftCell.trim() } : {}),
|
||||
})
|
||||
@@ -125,8 +171,19 @@ export function CustomersPage() {
|
||||
function clearFilters() {
|
||||
setDraftName('')
|
||||
setDraftCell('')
|
||||
setDraftAccess('all')
|
||||
setPage(1)
|
||||
setAppliedFilters({})
|
||||
setAppliedFilters({ access: 'all' })
|
||||
}
|
||||
|
||||
function handleAccessFilterChange(value: CustomerAccessFilter) {
|
||||
setDraftAccess(value)
|
||||
setPage(1)
|
||||
setAppliedFilters({
|
||||
access: value,
|
||||
...(draftName.trim() ? { name: draftName.trim() } : {}),
|
||||
...(draftCell.trim() ? { cellNumber: draftCell.trim() } : {}),
|
||||
})
|
||||
}
|
||||
|
||||
async function handleToggleEnabled(customer: BusinessCustomerListItem, isEnabled: boolean) {
|
||||
@@ -201,6 +258,22 @@ export function CustomersPage() {
|
||||
showToast(t('customers.toast.updated', { name: displayName(updated) }), 'success')
|
||||
}
|
||||
|
||||
function handleAccessSaved(updated: BusinessCustomerListItem) {
|
||||
setData((prev) => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
items: prev.items.map((item) => (item.id === updated.id ? { ...item, ...updated } : item)),
|
||||
}
|
||||
})
|
||||
const roleLabel = updated.isBusinessOwner
|
||||
? t('customers.access.badge.owner')
|
||||
: updated.teamRole
|
||||
? t(`customers.access.role.${updated.teamRole}`)
|
||||
: t('customers.access.customer')
|
||||
showToast(t('customers.toast.accessUpdated', { role: roleLabel }), 'success')
|
||||
}
|
||||
|
||||
function handleCustomerCreated(customer: BusinessCustomerListItem) {
|
||||
setPage(1)
|
||||
setData((prev) => {
|
||||
@@ -234,17 +307,28 @@ export function CustomersPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className={pageStyles.pageHeader}>
|
||||
<div className={`${pageStyles.pageHeader} ${styles.pageHeaderCompact}`}>
|
||||
<div>
|
||||
<h2 className={pageStyles.pageTitle}>{t('customers.title')}</h2>
|
||||
<p className={pageStyles.pageSubtitle}>{t('customers.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={filterStyles.filtersPanel}>
|
||||
<div className={filterStyles.filtersTitle}>{t('customers.filters')}</div>
|
||||
<form className={filterStyles.filtersGrid} onSubmit={(e) => { e.preventDefault(); applyFilters() }}>
|
||||
<div className={filterStyles.filtersInputs}>
|
||||
<div className={`${filterStyles.field} ${filterStyles.fieldCol3}`}>
|
||||
<select
|
||||
id="filter-customer-access"
|
||||
value={draftAccess}
|
||||
onChange={(e) => handleAccessFilterChange(e.target.value as CustomerAccessFilter)}
|
||||
aria-label={t('customers.filter.access')}
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="all">{t('customers.filter.access.all')}</option>
|
||||
<option value="customers">{t('customers.filter.access.customers')}</option>
|
||||
<option value="managers">{t('customers.filter.access.managers')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={`${filterStyles.field} ${filterStyles.fieldCol3}`}>
|
||||
<input
|
||||
id="filter-customer-name"
|
||||
@@ -355,6 +439,8 @@ export function CustomersPage() {
|
||||
data?.items?.map((customer) => {
|
||||
const name = displayName(customer)
|
||||
const nameLocale = textLocaleAttrs(name)
|
||||
const badgeKey = accessBadgeKey(customer)
|
||||
const accessLocked = accessChangeDisabledReason(customer)
|
||||
return (
|
||||
<tr
|
||||
key={customer.id}
|
||||
@@ -374,6 +460,9 @@ export function CustomersPage() {
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
{badgeKey ? (
|
||||
<span className={accessStyles.roleBadge}>{t(badgeKey)}</span>
|
||||
) : null}
|
||||
{!customer.isEnabled && (
|
||||
<div className={styles.statusDisabled}>{t('customers.disabled')}</div>
|
||||
)}
|
||||
@@ -417,6 +506,19 @@ export function CustomersPage() {
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{canManageAccess ? (
|
||||
<Tooltip label={accessLocked ?? t('customers.access.change')}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.actionBtn}
|
||||
onClick={() => setAccessTarget(customer)}
|
||||
aria-label={accessLocked ?? t('customers.access.change')}
|
||||
disabled={Boolean(accessLocked) || removing}
|
||||
>
|
||||
<Shield size={15} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
<Tooltip label={t('customers.edit')}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -499,6 +601,14 @@ export function CustomersPage() {
|
||||
onSaved={handleCustomerSaved}
|
||||
/>
|
||||
|
||||
<ChangeUserAccessModal
|
||||
open={accessTarget !== null}
|
||||
user={accessTarget}
|
||||
canAssignAdmin={isSuperAdmin}
|
||||
onClose={() => setAccessTarget(null)}
|
||||
onSaved={handleAccessSaved}
|
||||
/>
|
||||
|
||||
<ConfirmDeleteModal
|
||||
open={removeTarget !== null}
|
||||
title={t('customers.deleteTitle')}
|
||||
|
||||
@@ -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