Editors can pick product, store, blog, portfolio, or video category links, and image cards expose compact info/link actions without dumping URLs in the footer. Co-authored-by: Cursor <cursoragent@cursor.com>
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
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'
|
|
|
|
interface SearchableSelectProps {
|
|
options: FlatCategory[]
|
|
value: string
|
|
onChange: (value: string) => void
|
|
placeholder?: string
|
|
allowEmpty?: boolean
|
|
}
|
|
|
|
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,
|
|
allowEmpty = true,
|
|
}: 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()
|
|
if (!q) return true
|
|
return (
|
|
cat.nameEn.toLowerCase().includes(q) ||
|
|
cat.nameFa.includes(query.trim())
|
|
)
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (!open) return
|
|
const onClickOutside = (e: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
setOpen(false)
|
|
setQuery('')
|
|
}
|
|
}
|
|
document.addEventListener('mousedown', onClickOutside)
|
|
return () => document.removeEventListener('mousedown', onClickOutside)
|
|
}, [open])
|
|
|
|
function selectOption(id: string) {
|
|
onChange(id)
|
|
setOpen(false)
|
|
setQuery('')
|
|
}
|
|
|
|
function handleFocus() {
|
|
setOpen(true)
|
|
}
|
|
|
|
return (
|
|
<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 ? 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={t('categories.select.clear')}
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
)}
|
|
<ChevronDown size={16} className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`} />
|
|
</div>
|
|
|
|
{open && (
|
|
<ul className={styles.dropdown} role="listbox">
|
|
{allowEmpty ? (
|
|
<li>
|
|
<button
|
|
type="button"
|
|
className={`${styles.option} ${value === '' ? styles.optionSelected : ''}`}
|
|
onClick={() => selectOption('')}
|
|
>
|
|
{t('categories.select.none')}
|
|
</button>
|
|
</li>
|
|
) : null}
|
|
{filtered.length === 0 ? (
|
|
<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={{ paddingInlineStart: `${14 + cat.depth * 16}px` }}
|
|
onClick={() => selectOption(cat.id)}
|
|
>
|
|
{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>
|
|
))
|
|
)}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|