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
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user