Add customer and business user-product flows across dashboards.
Ship my-products / customer-products UI with gallery uploads, status controls, module gating, and related shared UI polish. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
c5bdaad16b
commit
1271d96539
@@ -0,0 +1,190 @@
|
||||
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 { 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>
|
||||
<h2 className={pageStyles.pageTitle}>{t('myProducts.title')}</h2>
|
||||
<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 ? (
|
||||
<div className={styles.empty}>
|
||||
<Package size={32} />
|
||||
<p>{t('myProducts.empty')}</p>
|
||||
<Link to="/my-products/new" className={styles.emptyLink}>
|
||||
{t('myProducts.add')}
|
||||
</Link>
|
||||
</div>
|
||||
) : 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user