Improve product AI UX, category search, and card actions.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
d787a69195
commit
b743c09e4d
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user