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:
co-authored by
Cursor
parent
7b2ed7c7b0
commit
a412cb82fc
@@ -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;
|
||||
|
||||
@@ -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}`}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user