Improve product AI UX, category search, and card actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-19 08:20:34 +03:30
co-authored by Cursor
parent d787a69195
commit b743c09e4d
15 changed files with 558 additions and 186 deletions
@@ -1,3 +1,67 @@
.listSection {
display: flex;
flex-direction: column;
gap: 14px;
}
.searchWrap {
position: relative;
display: flex;
align-items: center;
}
.searchIcon {
position: absolute;
inset-inline-start: 18px;
color: var(--text-muted);
pointer-events: none;
}
.searchInput {
width: 100%;
height: 52px;
padding-inline: 52px 48px;
font-size: 15px;
font-weight: 500;
color: var(--text-primary);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: var(--glass-shadow);
outline: none;
transition: border-color 0.15s, box-shadow 0.15s;
}
.searchInput::placeholder {
color: var(--text-muted);
font-weight: 400;
}
.searchInput:focus {
border-color: rgba(var(--primary-rgb) / 0.45);
box-shadow:
var(--glass-shadow),
0 0 0 3px rgba(var(--primary-rgb) / 0.12);
}
.searchClear {
position: absolute;
inset-inline-end: 12px;
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 10px;
color: var(--text-secondary);
transition: background 0.15s, color 0.15s;
}
.searchClear:hover {
background: rgba(var(--primary-rgb) / 0.1);
color: var(--primary);
}
.list {
background: var(--glass-bg);
backdrop-filter: blur(20px);
+84 -26
View File
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react'
import { FolderTree, Globe, Plus, Sparkles } from 'lucide-react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { FolderTree, Globe, Plus, Search, Sparkles, X } from 'lucide-react'
import { useToast } from '../context/ToastContext'
import { CategoryModal } from '../components/CategoryModal'
import { CategoryAiPromptModal } from '../components/CategoryAiPromptModal'
@@ -38,6 +38,7 @@ import {
toCategoryFormPayload,
updateProductCategory,
} from '../services/productCategoryService'
import { filterCategoriesByQuery, getAncestorIds } from '../utils/categories'
import { useT } from '../i18n/useT'
import { translateAllProductCategories } from '../services/translationService'
import type { TranslationLocale } from '../types/translation'
@@ -52,6 +53,7 @@ export function CategoriesPage() {
const { enabledModules } = useTenantBranding()
const isMultiLanguageEnabled = hasBusinessModule(enabledModules, 'multilanguage_data')
const [categories, setCategories] = useState<Category[]>([])
const [searchQuery, setSearchQuery] = useState('')
const [expandedIds, setExpandedIds] = useState<Set<string>>(() => new Set())
const [modalOpen, setModalOpen] = useState(false)
const [defaultParentId, setDefaultParentId] = useState('')
@@ -119,6 +121,32 @@ export function CategoriesPage() {
}
}, [])
const filteredCategories = useMemo(
() => filterCategoriesByQuery(categories, searchQuery),
[categories, searchQuery],
)
useEffect(() => {
const query = searchQuery.trim()
if (!query) return
const normalized = query.toLowerCase()
const nextExpanded = new Set<string>()
for (const category of categories) {
const haystack = `${category.nameFa} ${category.nameEn}`.toLowerCase()
if (!haystack.includes(normalized)) continue
for (const ancestorId of getAncestorIds(categories, category.id)) {
nextExpanded.add(ancestorId)
}
}
if (nextExpanded.size === 0) return
setExpandedIds((prev) => {
const merged = new Set(prev)
for (const id of nextExpanded) merged.add(id)
return merged
})
}, [categories, searchQuery])
function openAiFabMenu() {
if (aiMenuCloseTimerRef.current !== null) {
window.clearTimeout(aiMenuCloseTimerRef.current)
@@ -549,30 +577,60 @@ export function CategoriesPage() {
onAction={() => openCreateModal()}
/>
) : (
<div className={styles.list}>
<CategoryTree
categories={categories}
expandedIds={expandedIds}
variationCounts={variationCountsMap()}
technicalFieldCounts={technicalFieldCountsMap()}
onToggle={toggleExpanded}
onAddSub={(id) => openCreateModal(id, t('categories.addSub'))}
onVariations={openVariations}
onTechnicalForm={openTechnicalForm}
onEdit={(id) => {
const category = categories.find((item) => item.id === id)
if (category) openEditModal(category)
}}
onTranslate={(id) => {
if (!isMultiLanguageEnabled) return
const category = categories.find((item) => item.id === id)
if (category) setTranslateTarget(category)
}}
onRemove={(id) => {
const category = categories.find((item) => item.id === id)
if (category) setDeleteTarget(category)
}}
/>
<div className={styles.listSection}>
<div className={styles.searchWrap}>
<Search size={20} className={styles.searchIcon} aria-hidden="true" />
<input
type="search"
className={styles.searchInput}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder={t('categories.searchPlaceholder')}
aria-label={t('categories.search')}
/>
{searchQuery ? (
<button
type="button"
className={styles.searchClear}
onClick={() => setSearchQuery('')}
aria-label={t('categories.searchClear')}
>
<X size={18} />
</button>
) : null}
</div>
{filteredCategories.length === 0 ? (
<div className={styles.list}>
<p className={styles.empty}>{t('categories.searchEmpty')}</p>
</div>
) : (
<div className={styles.list}>
<CategoryTree
categories={filteredCategories}
expandedIds={expandedIds}
variationCounts={variationCountsMap()}
technicalFieldCounts={technicalFieldCountsMap()}
onToggle={toggleExpanded}
onAddSub={(id) => openCreateModal(id, t('categories.addSub'))}
onVariations={openVariations}
onTechnicalForm={openTechnicalForm}
onEdit={(id) => {
const category = categories.find((item) => item.id === id)
if (category) openEditModal(category)
}}
onTranslate={(id) => {
if (!isMultiLanguageEnabled) return
const category = categories.find((item) => item.id === id)
if (category) setTranslateTarget(category)
}}
onRemove={(id) => {
const category = categories.find((item) => item.id === id)
if (category) setDeleteTarget(category)
}}
/>
</div>
)}
</div>
)}
@@ -8,7 +8,6 @@ import { Pagination } from '../components/Pagination'
import { ConfirmDeleteModal } from '../components/ConfirmDeleteModal'
import { ProductVariantsModal } from '../components/ProductVariantsModal'
import { ProductTechnicalInfoModal } from '../components/ProductTechnicalInfoModal'
import { ProductCommentsModal } from '../components/ProductCommentsModal'
import { TranslationModal } from '../components/TranslationModal'
import { TranslateAllLanguageModal } from '../components/TranslateAllLanguageModal'
import { Breadcrumbs } from '../components/Breadcrumbs'
@@ -53,14 +52,12 @@ export function MyProductsPage() {
const [isDeleting, setIsDeleting] = useState(false)
const [error, setError] = useState('')
const [variantCounts, setVariantCounts] = useState<Record<string, number>>({})
const [commentCounts, setCommentCounts] = useState<Record<string, number>>({})
const [deleteTarget, setDeleteTarget] = useState<{ id: string; name: string } | null>(null)
const [variantsTarget, setVariantsTarget] = useState<{
id: string
name: string
categoryId: string
} | null>(null)
const [commentsTarget, setCommentsTarget] = useState<{ id: string; name: string } | null>(null)
const [technicalTarget, setTechnicalTarget] = useState<{
id: string
name: string
@@ -208,13 +205,6 @@ export function MyProductsPage() {
}
}
function handleComments(id: string) {
const product = products.find((p) => p.id === id)
if (product) {
setCommentsTarget({ id, name: product.nameFa || product.nameEn })
}
}
function handleVariations(id: string) {
const product = products.find((p) => p.id === id)
if (product) {
@@ -283,14 +273,6 @@ export function MyProductsPage() {
writeListQuery('', 1)
}
function handleCommentCountChange(count: number) {
if (!commentsTarget) return
setCommentCounts((prev) => ({
...prev,
[commentsTarget.id]: count,
}))
}
return (
<main className={pageStyles.content}>
<Breadcrumbs
@@ -374,12 +356,9 @@ export function MyProductsPage() {
<ProductCard
key={product.id}
product={product}
commentCount={commentCounts[product.id] ?? product.commentCount}
variantCount={variantCounts[product.id] ?? product.variantCount ?? 0}
onEdit={handleEdit}
onTranslate={handleTranslate}
onQuickInfo={() => {}}
onComments={handleComments}
onTechnicalInfo={handleTechnicalInfo}
onVariations={handleVariations}
onRemove={handleRemoveRequest}
@@ -435,16 +414,6 @@ export function MyProductsPage() {
/>
)}
{commentsTarget && (
<ProductCommentsModal
open={!!commentsTarget}
productId={commentsTarget.id}
productName={commentsTarget.name}
onClose={() => setCommentsTarget(null)}
onCountChange={handleCommentCountChange}
/>
)}
{technicalTarget && (
<ProductTechnicalInfoModal
open={!!technicalTarget}