Improve dashboard charts, category tree selects, and product AI name fill.

Fix RTL chart ordering with Y-axis and hover values; aggregate website views; refine category picker tree borders and spacing; add one-click FA→EN name translation on add product.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-28 20:39:43 +03:30
co-authored by Cursor
parent 7b2ed7c7b0
commit a412cb82fc
31 changed files with 1222 additions and 226 deletions
@@ -106,6 +106,18 @@
z-index: 2;
}
.aiFillAnchorInline {
position: absolute;
top: 50%;
inset-inline-end: 6px;
transform: translateY(-50%);
z-index: 2;
}
.nameEnField input {
padding-inline-end: 44px;
}
.aiFillBtn {
width: 30px;
height: 30px;
+53 -14
View File
@@ -26,7 +26,6 @@ import {
} from '../services/productService'
import {
fillProductFieldByAi,
type ProductAiFillField,
} from '../services/productAiService'
import type { Category, FlatCategory } from '../types/category'
import { flattenCategories } from '../utils/categories'
@@ -58,7 +57,7 @@ export function AddNewProductPage() {
const [loaded, setLoaded] = useState(!isEdit)
const [isSubmitting, setIsSubmitting] = useState(false)
const [error, setError] = useState('')
const [aiField, setAiField] = useState<ProductAiFillField | null>(null)
const [aiField, setAiField] = useState<'summary' | 'description' | null>(null)
const [isAiRunning, setIsAiRunning] = useState(false)
const categoryOptions = flattenCategories(categories)
@@ -69,8 +68,9 @@ export function AddNewProductPage() {
}, [categoryOptions, categoryId])
const canUseAiFill = Boolean(nameFa.trim() || nameEn.trim()) && !isSubmitting
const canTranslateNameEn = Boolean(nameFa.trim()) && !isSubmitting && !isAiRunning
function openAiFill(field: ProductAiFillField) {
function openAiFill(field: 'summary' | 'description') {
if (!nameFa.trim() && !nameEn.trim()) {
setError(t('products.form.aiFillNeedName'))
return
@@ -103,6 +103,30 @@ export function AddNewProductPage() {
}
}
async function translateNameEnByAi() {
if (!nameFa.trim()) {
setError(t('products.form.aiTranslateNameNeedFa'))
return
}
setError('')
setIsAiRunning(true)
try {
const result = await fillProductFieldByAi({
field: 'nameEn',
nameFa: nameFa.trim(),
categoryId: categoryId || undefined,
})
if (result.field === 'nameEn') {
setNameEn(result.text)
showToast(t('products.form.aiNameEnFilled'), 'success')
}
} catch (err) {
setError(err instanceof ApiError ? err.message : t('products.form.aiFillError'))
} finally {
setIsAiRunning(false)
}
}
useEffect(() => {
const controller = new AbortController()
@@ -302,17 +326,32 @@ export function AddNewProductPage() {
<div className={`${styles.field} ${styles.fieldHalf}`}>
<label htmlFor="nameEn">{t('products.form.nameEn')}</label>
<input
id="nameEn"
name="nameEn"
type="text"
dir="ltr"
placeholder={t('products.form.nameEnPlaceholder')}
value={nameEn}
onChange={(e) => setNameEn(e.target.value)}
required
disabled={isSubmitting}
/>
<div className={`${styles.fieldWithAi} ${styles.nameEnField}`}>
<input
id="nameEn"
name="nameEn"
type="text"
dir="ltr"
placeholder={t('products.form.nameEnPlaceholder')}
value={nameEn}
onChange={(e) => setNameEn(e.target.value)}
required
disabled={isSubmitting || isAiRunning}
/>
<span className={styles.aiFillAnchorInline}>
<Tooltip label={t('products.form.aiTranslateName')}>
<button
type="button"
className={`${aiStyles.aiBtn} ${styles.aiFillBtn}`}
onClick={() => void translateNameEnByAi()}
disabled={!canTranslateNameEn}
aria-label={t('products.form.aiTranslateName')}
>
<Sparkles size={14} />
</button>
</Tooltip>
</span>
</div>
</div>
<div className={`${styles.field} ${styles.fieldFull} ${styles.summaryField}`}>
+9 -6
View File
@@ -60,6 +60,7 @@ export function CategoriesPage() {
const [categoryVariations, setCategoryVariations] = useState<Record<string, Variation[]>>({})
const [variationsLoading, setVariationsLoading] = useState(false)
const [variationsSaving, setVariationsSaving] = useState(false)
const [variationsError, setVariationsError] = useState('')
const [technicalTarget, setTechnicalTarget] = useState<{
categoryId: string
categoryName: string
@@ -233,7 +234,7 @@ export function CategoriesPage() {
: category.nameEn,
})
setVariationsLoading(true)
setError('')
setVariationsError('')
try {
const items = await listCategoryVariations(categoryId)
@@ -241,9 +242,9 @@ export function CategoriesPage() {
setVariationCount(categoryId, items.length)
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
setVariationsError(err.message)
} else {
setError('Unable to load category variations.')
setVariationsError('Unable to load category variations.')
}
} finally {
setVariationsLoading(false)
@@ -394,7 +395,7 @@ export function CategoriesPage() {
if (!variationsTarget) return
setVariationsSaving(true)
setError('')
setVariationsError('')
try {
const saved = await saveCategoryVariations(
@@ -409,9 +410,9 @@ export function CategoriesPage() {
setVariationsTarget(null)
} catch (err) {
if (err instanceof ApiError) {
setError(err.message)
setVariationsError(err.message)
} else {
setError('Unable to save category variations.')
setVariationsError('Unable to save category variations.')
}
} finally {
setVariationsSaving(false)
@@ -514,8 +515,10 @@ export function CategoriesPage() {
: getVariationsForCategory(variationsTarget.categoryId)
}
isSaving={variationsSaving || variationsLoading}
error={variationsError}
onClose={() => !variationsSaving && setVariationsTarget(null)}
onChange={(variations) => {
setVariationsError('')
setCategoryVariations((prev) => ({
...prev,
[variationsTarget.categoryId]: variations,
+5
View File
@@ -91,6 +91,11 @@ export function LoginPage() {
err.message.includes('Invalid cell number or password')
) {
setError(t('login.error.invalidCredentials'))
} else if (
err.message.includes('Cell number is not verified') ||
err.message.includes('complete OTP verification')
) {
setError(t('login.error.cellNotVerified'))
} else {
setError(err.message)
}