Files
dashboards/apps/business/src/pages/BrandsPage.tsx
T
Alireza HassaniandCursor 6c52ce1247 Polish dashboard page chrome and user access UI.
Rename Customers to Users with FA/EN titles, compact headers and filter bars across apps, and refine manager role copy plus a customer-header link to the business dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 00:00:09 +03:30

221 lines
6.2 KiB
TypeScript

import { useEffect, useState } from 'react'
import { Plus } from 'lucide-react'
import { useToast } from '../context/ToastContext'
import { BrandModal } from '../components/BrandModal'
import { BrandRow } from '../components/BrandRow'
import { Breadcrumbs } from '../components/Breadcrumbs'
import { PageTitle } from '../components/PageTitle'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import type { Brand, BrandFormData } from '../types/brand'
import { ApiError } from '../lib/api'
import { resolveDataUrlToMediaId } from '../services/mediaService'
import {
createBrand,
deleteBrand,
listAllBrands,
mapBrandApiToUi,
toBrandFormPayload,
updateBrand,
} from '../services/brandService'
import { useT } from '../i18n/useT'
import pageStyles from '../components/PageContent.module.css'
import styles from './BrandsPage.module.css'
export function BrandsPage() {
const t = useT()
const { showToast } = useToast()
const [brands, setBrands] = useState<Brand[]>([])
const [modalOpen, setModalOpen] = useState(false)
const [modalTitle, setModalTitle] = useState(() => t('brands.add'))
const [editingBrand, setEditingBrand] = useState<Brand | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const [deleteTarget, setDeleteTarget] = useState<Brand | null>(null)
useEffect(() => {
const controller = new AbortController()
void loadBrands(controller.signal)
return () => controller.abort()
}, [])
async function loadBrands(signal?: AbortSignal) {
setIsLoading(true)
setError('')
try {
const items = await listAllBrands(signal)
setBrands(items.map(mapBrandApiToUi))
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) {
setError(err.message)
} else {
setError(t('brands.errorLoad'))
}
} finally {
setIsLoading(false)
}
}
function openCreateModal() {
setEditingBrand(null)
setModalTitle(t('brands.add'))
setModalOpen(true)
}
function openEditModal(brand: Brand) {
setEditingBrand(brand)
setModalTitle(t('brands.edit'))
setModalOpen(true)
}
async function handleSubmit(data: BrandFormData) {
setIsSubmitting(true)
setError('')
try {
const resolvedImageMediaId = await resolveDataUrlToMediaId(
data.image,
'brand-logo.png',
data.imageMediaId,
)
if (editingBrand) {
const result = await updateBrand(editingBrand.id, {
...toBrandFormPayload(data),
imageMediaId: resolvedImageMediaId,
})
setBrands((prev) =>
prev.map((brand) =>
brand.id === editingBrand.id ? mapBrandApiToUi(result.brand) : brand,
),
)
showToast(t('brands.toast.updated'), 'success')
} else {
const result = await createBrand(toBrandFormPayload(data, resolvedImageMediaId))
setBrands((prev) => [...prev, mapBrandApiToUi(result.brand)])
showToast(t('brands.toast.created'), 'success')
}
setModalOpen(false)
setEditingBrand(null)
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
} else {
setError(t('brands.errorSave'))
}
} finally {
setIsSubmitting(false)
}
}
async function confirmDelete() {
if (!deleteTarget) return
setIsSubmitting(true)
setError('')
try {
await deleteBrand(deleteTarget.id)
setBrands((prev) => prev.filter((brand) => brand.id !== deleteTarget.id))
setDeleteTarget(null)
showToast(t('brands.toast.deleted'), 'success')
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
} else {
setError(t('brands.errorDelete'))
}
} finally {
setIsSubmitting(false)
}
}
return (
<main className={pageStyles.content}>
<Breadcrumbs
items={[
{ label: 'Dashboard', href: '/' },
{ label: 'Products', href: '/products' },
{ label: 'Brands' },
]}
/>
<div className={pageStyles.pageHeader}>
<div>
<PageTitle en="BRANDS">{t('title.brands')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('brands.page.subtitle')}</p>
</div>
</div>
{error && (
<div className={styles.error} role="alert">
{error}
</div>
)}
<div className={styles.list}>
{isLoading ? (
<p className={styles.empty}>{t('brands.loading')}</p>
) : brands.length === 0 ? (
<p className={styles.empty}>{t('brands.empty')}</p>
) : (
brands.map((brand) => (
<BrandRow
key={brand.id}
brand={brand}
onEdit={(id) => {
const item = brands.find((entry) => entry.id === id)
if (item) openEditModal(item)
}}
onRemove={(id) => {
const item = brands.find((entry) => entry.id === id)
if (item) setDeleteTarget(item)
}}
/>
))
)}
</div>
<BrandModal
open={modalOpen}
onClose={() => {
if (!isSubmitting) {
setModalOpen(false)
setEditingBrand(null)
}
}}
onSubmit={handleSubmit}
editingBrand={editingBrand}
title={modalTitle}
isSubmitting={isSubmitting}
/>
<ConfirmDeleteModal
open={!!deleteTarget}
title={t('brands.deleteTitle')}
message={
deleteTarget
? t('brands.deleteMessage', {
name: deleteTarget.nameFa || deleteTarget.nameEn,
})
: ''
}
onConfirm={confirmDelete}
onCancel={() => !isSubmitting && setDeleteTarget(null)}
/>
<div className={styles.fabDock}>
<button
type="button"
className={styles.addFab}
onClick={openCreateModal}
aria-label={t('brands.add')}
>
<Plus size={24} />
</button>
</div>
</main>
)
}