Files
dashboards/apps/business/src/components/SearchableSelect.tsx
T
Alireza HassaniandCursor e0e68de30f Add a reusable website link generator and polish static image cards.
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>
2026-08-17 00:30:01 +03:30

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>
)
}