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
@@ -1,11 +1,12 @@
import { useEffect, useRef, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { Plus, X } from 'lucide-react'
import { Plus, Search, X } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { useT } from '../i18n/useT'
import type { BusinessMessageKey } from '../i18n/messages'
import { COLOR_PRESETS } from '../data/colorPresets'
import type { Variation, VariationType } from '../types/variation'
import { COLOR_OPTIONS, SIZE_OPTIONS } from '../types/variation'
import { SIZE_OPTIONS, normalizeColorNames } from '../types/variation'
import { Tooltip } from './Tooltip'
import styles from './VariationsModal.module.css'
@@ -24,6 +25,10 @@ function seedCustomValues(values: string[]) {
return trimmed.length ? trimmed : ['']
}
function colorLabelKey(name: string): BusinessMessageKey {
return `categories.variations.color.${name}` as BusinessMessageKey
}
export function AddVariationModal({ open, onClose, onSubmit, initial = null }: AddVariationModalProps) {
const t = useT()
const { locale } = useLocale()
@@ -36,6 +41,7 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
const [selectedColors, setSelectedColors] = useState<string[]>([])
const [selectedSizes, setSelectedSizes] = useState<string[]>([])
const [customValues, setCustomValues] = useState<string[]>([''])
const [colorQuery, setColorQuery] = useState('')
const valueInputRefs = useRef<(HTMLInputElement | null)[]>([])
const pendingFocusIndex = useRef<number | null>(null)
@@ -49,10 +55,13 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
if (open) {
setMounted(true)
setClosing(false)
setColorQuery('')
if (initial) {
setType(initial.type)
setCustomName(initial.type === 'custom' ? initial.name : '')
setSelectedColors(initial.type === 'color' ? [...initial.values] : [])
setSelectedColors(
initial.type === 'color' ? normalizeColorNames(initial.values) : [],
)
setSelectedSizes(initial.type === 'size' ? [...initial.values] : [])
setCustomValues(
initial.type === 'custom' ? seedCustomValues(initial.values) : [''],
@@ -83,12 +92,22 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
return () => document.removeEventListener('keydown', onKey)
}, [mounted, closing, onClose])
const filteredColors = useMemo(() => {
const q = colorQuery.trim().toLowerCase()
if (!q) return COLOR_PRESETS
return COLOR_PRESETS.filter((color) => {
const en = color.name.toLowerCase()
const fa = t(colorLabelKey(color.name)).toLowerCase()
return en.includes(q) || fa.includes(q)
})
}, [colorQuery, t])
if (!mounted) return null
function toggleOption(value: string, selected: string[], setSelected: (v: string[]) => void) {
setSelected(
selected.includes(value)
? selected.filter((v) => v !== value)
? selected.filter((item) => item !== value)
: [...selected, value],
)
}
@@ -180,18 +199,78 @@ export function AddVariationModal({ open, onClose, onSubmit, initial = null }: A
{type === 'color' && (
<div className={styles.field}>
<label>{t('categories.variations.selectColors')}</label>
<div className={styles.chipGrid}>
{COLOR_OPTIONS.map((color) => (
<label htmlFor="colorSearch">{t('categories.variations.selectColors')}</label>
<div className={styles.colorSearchWrap}>
<Search size={16} className={styles.colorSearchIcon} aria-hidden="true" />
<input
id="colorSearch"
type="search"
className={styles.colorSearchInput}
value={colorQuery}
onChange={(e) => setColorQuery(e.target.value)}
placeholder={t('categories.variations.searchColors')}
autoComplete="off"
/>
{colorQuery ? (
<button
key={color}
type="button"
className={`${styles.chip} ${selectedColors.includes(color) ? styles.chipSelected : ''}`}
onClick={() => toggleOption(color, selectedColors, setSelectedColors)}
className={styles.colorSearchClear}
onClick={() => setColorQuery('')}
aria-label={t('common.close')}
>
{t(`categories.variations.color.${color}` as BusinessMessageKey)}
<X size={14} />
</button>
))}
) : null}
</div>
{selectedColors.length > 0 ? (
<div className={styles.selectedColors}>
{selectedColors.map((color) => {
const preset = COLOR_PRESETS.find((item) => item.name === color)
return (
<button
key={color}
type="button"
className={`${styles.chip} ${styles.chipSelected}`}
onClick={() =>
toggleOption(color, selectedColors, setSelectedColors)
}
>
<span
className={styles.colorSwatch}
style={{ backgroundColor: preset?.hex ?? '#94a3b8' }}
aria-hidden="true"
/>
{t(colorLabelKey(color))}
<X size={12} aria-hidden="true" />
</button>
)
})}
</div>
) : null}
<div className={styles.chipGrid}>
{filteredColors.length === 0 ? (
<p className={styles.colorEmpty}>{t('categories.variations.noColorMatch')}</p>
) : (
filteredColors.map((color) => (
<button
key={color.name}
type="button"
className={`${styles.chip} ${
selectedColors.includes(color.name) ? styles.chipSelected : ''
}`}
onClick={() =>
toggleOption(color.name, selectedColors, setSelectedColors)
}
>
<span
className={styles.colorSwatch}
style={{ backgroundColor: color.hex }}
aria-hidden="true"
/>
{t(colorLabelKey(color.name))}
</button>
))
)}
</div>
</div>
)}