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
@@ -4,7 +4,7 @@ import styles from './AddressListEditor.module.css'
export type CityOption = {
id: string
parentId: string | null
level: 'country' | 'province' | 'city'
level: 'country' | 'province' | 'city' | 'district'
nameFa: string
nameEn: string
landlineCode: string | null
@@ -4,9 +4,16 @@
align-items: center;
height: 38px;
border-radius: 50px;
background: rgba(255, 255, 255, 0.5);
background: linear-gradient(
145deg,
color-mix(in srgb, var(--elevated-surface) 78%, transparent) 0%,
color-mix(in srgb, var(--glass-bg) 55%, transparent) 55%,
color-mix(in srgb, var(--surface) 42%, transparent) 100%
);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
border: 1px solid var(--glass-border);
transition: box-shadow 0.2s, border-color 0.2s;
transition: box-shadow 0.2s, border-color 0.2s, background 0.2s;
}
.wrap:hover,
@@ -20,6 +27,7 @@
inset-inline-start: 12px;
color: var(--text-secondary);
pointer-events: none;
z-index: 1;
}
.chevron {
@@ -27,27 +35,37 @@
inset-inline-end: 10px;
color: var(--text-muted);
pointer-events: none;
z-index: 1;
}
.select {
/* Beat global `select` + `html[data-theme='dark'] select` so the pill stays one surface */
.wrap .select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
height: 100%;
min-width: 72px;
min-height: 0;
min-width: 76px;
padding-block: 0;
padding-inline: 34px 28px;
border: none;
border-radius: 50px;
background: transparent;
background-color: transparent;
background-image: none;
box-shadow: none;
color: var(--text-primary);
font-size: 13px;
font-weight: 600;
font-family: var(--font-ui);
line-height: 1;
cursor: pointer;
outline: none;
transition: none;
}
.select:focus {
.wrap .select:focus {
border: none;
box-shadow: none;
outline: none;
}
@@ -1,6 +1,7 @@
.card {
display: flex;
flex-direction: column;
height: 100%;
padding: 28px;
background: var(--glass-bg);
backdrop-filter: blur(20px);
@@ -19,7 +20,8 @@
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, var(--primary-light) 0%, rgba(219, 234, 254, 0.5) 100%);
background: linear-gradient(135deg, var(--icon-bg) 0%, var(--icon-bg-end) 100%);
border: 1px solid rgba(var(--primary-rgb) / 0.22);
border-radius: var(--radius-sm);
color: var(--primary);
margin-bottom: 20px;
@@ -50,6 +52,9 @@
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 36px;
margin-top: auto;
}
.link {
@@ -58,28 +63,74 @@
color: var(--primary);
}
.countMeta {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 8px;
height: 36px;
max-width: calc(100% - 48px);
padding: 0 14px;
box-sizing: border-box;
border-radius: 999px;
background: rgba(var(--primary-rgb) / 0.1);
color: var(--primary);
transition:
background 0.35s ease,
color 0.35s ease;
}
.countValue {
font-family: var(--font-en), var(--font-ui), sans-serif;
font-size: 16px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: -0.02em;
line-height: 1;
color: inherit;
}
.countLabel {
font-size: 12px;
font-weight: 500;
line-height: 1;
color: inherit;
opacity: 0.85;
}
.card:hover .countMeta {
background: var(--primary);
color: white;
}
.card:hover .countLabel {
opacity: 0.95;
}
.arrowBtn {
width: 36px;
height: 36px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(var(--primary-rgb) / 0.1);
color: var(--primary);
transition: background 0.2s, transform 0.2s;
transition:
background 0.35s ease,
color 0.35s ease;
}
.arrowIcon {
display: block;
}
.card:hover .arrowBtn {
background: var(--primary);
color: white;
transform: translateX(2px);
}
:global([dir='rtl']) .arrowBtn {
transform: scaleX(-1);
}
:global([dir='rtl']) .card:hover .arrowBtn {
transform: scaleX(-1) translateX(2px);
}
@@ -6,8 +6,12 @@ interface SectionCardProps {
icon: LucideIcon
title: string
description: string
linkText: string
/** Legacy footer link text; prefer count + countLabel when available. */
linkText?: string
href: string
/** Entity total shown beside the arrow; omit for sections without a count. */
count?: number | null
countLabel?: string
}
export function SectionCard({
@@ -16,7 +20,14 @@ export function SectionCard({
description,
linkText,
href,
count,
countLabel,
}: SectionCardProps) {
const showCount = typeof count === 'number' && Number.isFinite(count)
const formattedCount = showCount
? new Intl.NumberFormat('en-US').format(count)
: null
return (
<Link to={href} className={styles.card} data-card-hover>
<div className={styles.iconWrap}>
@@ -27,9 +38,20 @@ export function SectionCard({
<p className={styles.description}>{description}</p>
<div className={styles.footer}>
<span className={styles.link}>{linkText}</span>
{formattedCount !== null && countLabel ? (
<div className={styles.countMeta}>
<span className={styles.countValue} lang="en" dir="ltr">
{formattedCount}
</span>
<span className={styles.countLabel}>{countLabel}</span>
</div>
) : linkText ? (
<span className={styles.link}>{linkText}</span>
) : (
<span />
)}
<span className={styles.arrowBtn} aria-hidden="true">
<ArrowRight size={18} />
<ArrowRight size={18} className={styles.arrowIcon} />
</span>
</div>
</Link>
@@ -19,7 +19,7 @@
font-weight: 500;
line-height: 1.4;
color: var(--text-primary);
background: rgba(255, 255, 255, 0.55);
background: var(--elevated-surface);
backdrop-filter: blur(var(--blur-glass));
-webkit-backdrop-filter: blur(var(--blur-glass));
border: 1px solid var(--glass-border);