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>
192 lines
5.4 KiB
TypeScript
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>
|
|
)
|
|
}
|