Files
dashboards/apps/customer/src/pages/MyProductsPage.tsx
T
Alireza HassaniandCursor d5aca8bea7 Add business workshop management UI with Jalali scheduling and shared dashboard polish.
Includes workshop list/edit/details, registrations modal, category tree, shared EmptyState/RichTextEditor/toasts, and fixes for the Jalali date picker and thumbnail field layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-22 12:22:27 +03:30

192 lines
5.4 KiB
TypeScript

import { PageTitle } from '../components/PageTitle'
import { useEffect, useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { Package, Plus } from 'lucide-react'
import { Breadcrumbs, Pagination, useToast } from '@meshkee/dashboard-ui'
import { EmptyState } from '../components/EmptyState'
import { UserProductCard } from '../components/UserProductCard'
import { useT } from '../i18n/useT'
import { ApiError, isAbortError } from '../lib/api'
import {
deleteMyUserProduct,
listMyUserProducts,
promoteMyUserProduct,
type MyUserProductsListResponse,
} from '../services/userProductsService'
import pageStyles from '../components/PageContent.module.css'
import styles from './MyProductsPage.module.css'
const PAGE_SIZE = 24
export function MyProductsPage() {
const t = useT()
const navigate = useNavigate()
const { showToast } = useToast()
const [data, setData] = useState<MyUserProductsListResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [page, setPage] = useState(1)
const [busyId, setBusyId] = useState<string | null>(null)
const [busyAction, setBusyAction] = useState<'remove' | 'promote' | null>(null)
useEffect(() => {
const controller = new AbortController()
async function load() {
setLoading(true)
setError('')
try {
const response = await listMyUserProducts(
{ page, pageSize: PAGE_SIZE },
controller.signal,
)
if (controller.signal.aborted) return
setData(response)
} catch (err) {
if (isAbortError(err) || controller.signal.aborted) return
setError(err instanceof ApiError ? err.message : t('myProducts.error.load'))
} finally {
if (!controller.signal.aborted) setLoading(false)
}
}
void load()
return () => controller.abort()
}, [page, t])
const products = data?.items ?? []
const total = data?.total ?? 0
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
function handleEdit(id: string) {
navigate(`/my-products/${id}/edit`)
}
async function handleRemove(id: string) {
if (!window.confirm(t('myProducts.removeConfirm'))) return
setBusyId(id)
setBusyAction('remove')
try {
await deleteMyUserProduct(id)
setData((prev) =>
prev
? {
...prev,
items: prev.items.filter((item) => item.id !== id),
total: Math.max(0, prev.total - 1),
}
: prev,
)
showToast(t('myProducts.removeSuccess'), 'success')
} catch (err) {
showToast(
err instanceof ApiError ? err.message : t('myProducts.error.remove'),
'error',
)
} finally {
setBusyId(null)
setBusyAction(null)
}
}
async function handlePromote(id: string) {
setBusyId(id)
setBusyAction('promote')
try {
const response = await promoteMyUserProduct(id)
setData((prev) =>
prev
? {
...prev,
items: prev.items.map((item) =>
item.id === id ? { ...item, ...response.product } : item,
),
}
: prev,
)
showToast(t('myProducts.promoteSuccess'), 'success')
} catch (err) {
showToast(
err instanceof ApiError ? err.message : t('myProducts.error.promote'),
'error',
)
} finally {
setBusyId(null)
setBusyAction(null)
}
}
return (
<main className={pageStyles.content}>
<Breadcrumbs
items={[
{ label: t('nav.home'), href: '/' },
{ label: t('myProducts.title') },
]}
/>
<div className={pageStyles.pageHeader}>
<div>
<PageTitle en="MY PRODUCTS">{t('myProducts.title')}</PageTitle>
<p className={pageStyles.pageSubtitle}>{t('myProducts.subtitle')}</p>
</div>
</div>
{error ? (
<div className={styles.error} role="alert">
{error}
</div>
) : null}
{loading ? <p className={styles.status}>{t('myProducts.loading')}</p> : null}
{!loading && !error && products.length === 0 ? (
<EmptyState
icon={Package}
message={t('myProducts.empty')}
actionLabel={t('myProducts.add')}
actionTo="/my-products/new"
/>
) : null}
{!loading && products.length > 0 ? (
<>
<div className={styles.grid}>
{products.map((product) => (
<UserProductCard
key={product.id}
product={product}
to={`/my-products/${product.id}`}
onEdit={handleEdit}
onRemove={handleRemove}
onPromote={handlePromote}
busyAction={busyId === product.id ? busyAction : null}
/>
))}
</div>
{totalPages > 1 ? (
<Pagination
currentPage={page}
totalPages={totalPages}
onPageChange={setPage}
disabled={loading}
/>
) : null}
</>
) : null}
<div className={styles.fabDock}>
<Link
to="/my-products/new"
className={styles.addFab}
aria-label={t('myProducts.add')}
title={t('myProducts.add')}
>
<Plus size={24} />
</Link>
</div>
</main>
)
}