Polish business FA layout and wire special-group keys plus SSL sync.
RTL carousels/FABs, special key+title fields, slider gallery fixes, and dashboard SSL sync agent for super-admin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
66004a0fba
commit
672091d1f5
@@ -1,5 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { ChevronDown, Search, X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import type { FlatCategory } from '../types/category'
|
||||
import styles from './SearchableSelect.module.css'
|
||||
|
||||
@@ -10,18 +12,34 @@ interface SearchableSelectProps {
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
function categoryLabel(cat: FlatCategory, isFa: boolean) {
|
||||
if (isFa) {
|
||||
return cat.nameFa
|
||||
? `${cat.nameFa}${cat.nameEn ? ` / ${cat.nameEn}` : ''}`
|
||||
: cat.nameEn
|
||||
}
|
||||
return cat.nameEn
|
||||
? `${cat.nameEn}${cat.nameFa ? ` / ${cat.nameFa}` : ''}`
|
||||
: cat.nameFa
|
||||
}
|
||||
|
||||
export function SearchableSelect({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Search categories...',
|
||||
placeholder,
|
||||
}: SearchableSelectProps) {
|
||||
const t = useT()
|
||||
const { locale } = useLocale()
|
||||
const isFa = locale === 'fa'
|
||||
const [open, setOpen] = useState(false)
|
||||
const [query, setQuery] = useState('')
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const selected = options.find((o) => o.id === value)
|
||||
const selectedLabel = selected ? categoryLabel(selected, isFa) : ''
|
||||
const searchPlaceholder = placeholder ?? t('categories.select.search')
|
||||
|
||||
const filtered = options.filter((cat) => {
|
||||
const q = query.trim().toLowerCase()
|
||||
@@ -55,27 +73,28 @@ export function SearchableSelect({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} ref={containerRef}>
|
||||
<div className={styles.wrapper} ref={containerRef} dir={isFa ? 'rtl' : 'ltr'}>
|
||||
<div className={`${styles.inputWrap} ${open ? styles.inputWrapOpen : ''}`}>
|
||||
<Search size={16} className={styles.searchIcon} />
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className={styles.input}
|
||||
placeholder={selected ? `${selected.nameEn} / ${selected.nameFa}` : placeholder}
|
||||
value={open ? query : selected ? `${selected.nameEn} / ${selected.nameFa}` : ''}
|
||||
placeholder={selected ? selectedLabel : searchPlaceholder}
|
||||
value={open ? query : selectedLabel}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value)
|
||||
if (!open) setOpen(true)
|
||||
}}
|
||||
onFocus={handleFocus}
|
||||
dir={isFa ? 'rtl' : 'ltr'}
|
||||
/>
|
||||
{value && !open && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearBtn}
|
||||
onClick={() => onChange('')}
|
||||
aria-label="Clear selection"
|
||||
aria-label={t('categories.select.clear')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -91,23 +110,41 @@ export function SearchableSelect({
|
||||
className={`${styles.option} ${value === '' ? styles.optionSelected : ''}`}
|
||||
onClick={() => selectOption('')}
|
||||
>
|
||||
— None (root category) —
|
||||
{t('categories.select.none')}
|
||||
</button>
|
||||
</li>
|
||||
{filtered.length === 0 ? (
|
||||
<li className={styles.noResults}>No categories found</li>
|
||||
<li className={styles.noResults}>{t('categories.select.empty')}</li>
|
||||
) : (
|
||||
filtered.map((cat) => (
|
||||
<li key={cat.id}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.option} ${value === cat.id ? styles.optionSelected : ''}`}
|
||||
style={{ paddingLeft: `${14 + cat.depth * 16}px` }}
|
||||
style={{ paddingInlineStart: `${14 + cat.depth * 16}px` }}
|
||||
onClick={() => selectOption(cat.id)}
|
||||
>
|
||||
<span className={styles.optionEn}>{cat.nameEn}</span>
|
||||
<span className={styles.optionSep}>·</span>
|
||||
<span className={styles.optionFa}>{cat.nameFa}</span>
|
||||
{isFa ? (
|
||||
<>
|
||||
<span className={styles.optionFa}>{cat.nameFa}</span>
|
||||
{cat.nameEn ? (
|
||||
<>
|
||||
<span className={styles.optionSep}>·</span>
|
||||
<span className={styles.optionEn}>{cat.nameEn}</span>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className={styles.optionEn}>{cat.nameEn}</span>
|
||||
{cat.nameFa ? (
|
||||
<>
|
||||
<span className={styles.optionSep}>·</span>
|
||||
<span className={styles.optionFa}>{cat.nameFa}</span>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user