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:
Alireza Hassani
2026-08-10 00:23:03 +03:30
co-authored by Cursor
parent c5bdaad16b
commit 1271d96539
96 changed files with 10532 additions and 345 deletions
+190
View File
@@ -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>
)
}