Files
dashboards/apps/business/src/components/ProductSearchSelect.tsx
T
Alireza HassaniandCursor 672091d1f5 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>
2026-08-03 00:06:44 +03:30

185 lines
5.8 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react'
import { ChevronDown, Search, X } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import { useT } from '../i18n/useT'
import styles from './SearchableSelect.module.css'
export interface ProductSearchOption {
id: string
title: string
nameFa: string
}
interface ProductSearchSelectProps {
options: ProductSearchOption[]
value: string
onChange: (value: string) => void
placeholder?: string
disabled?: boolean
id?: string
minSearchLength?: number
'aria-label'?: string
}
function formatProductLabel(option: ProductSearchOption, preferFa: boolean) {
if (preferFa && option.nameFa) {
return option.title ? `${option.nameFa} / ${option.title}` : option.nameFa
}
return option.nameFa ? `${option.title} / ${option.nameFa}` : option.title
}
export function ProductSearchSelect({
options,
value,
onChange,
placeholder = 'Search products...',
disabled = false,
id,
minSearchLength = 3,
'aria-label': ariaLabel,
}: ProductSearchSelectProps) {
const t = useT()
const { locale } = useLocale()
const preferFa = 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((option) => option.id === value)
const selectedLabel = selected ? formatProductLabel(selected, preferFa) : ''
const filtered = useMemo(() => {
const trimmed = query.trim()
if (trimmed.length < minSearchLength) return []
const q = trimmed.toLowerCase()
return options.filter(
(option) =>
option.title.toLowerCase().includes(q) ||
option.nameFa.includes(trimmed),
)
}, [options, query, minSearchLength])
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() {
if (!disabled) setOpen(true)
}
const showDropdown = open && query.trim().length >= minSearchLength
return (
<div className={styles.wrapper} ref={containerRef} dir={preferFa ? 'rtl' : 'ltr'}>
<div className={`${styles.inputWrap} ${open ? styles.inputWrapOpen : ''}`}>
<Search size={16} className={styles.searchIcon} />
<input
ref={inputRef}
id={id}
type="text"
className={styles.input}
placeholder={selected ? selectedLabel : placeholder}
value={open ? query : selectedLabel}
onChange={(e) => {
setQuery(e.target.value)
if (!open) setOpen(true)
if (value) onChange('')
}}
onFocus={handleFocus}
disabled={disabled}
aria-label={ariaLabel ?? placeholder}
dir={preferFa ? 'rtl' : 'ltr'}
/>
{value && !open && (
<button
type="button"
className={styles.clearBtn}
onClick={() => onChange('')}
aria-label={t('storeItems.search.clear')}
>
<X size={14} />
</button>
)}
<ChevronDown size={16} className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`} />
</div>
{showDropdown && (
<ul className={`${styles.dropdown} ${styles.dropdownInFlow}`} role="listbox">
{filtered.length === 0 ? (
<li className={styles.noResults}>
{options.length === 0
? t('storeItems.search.noProducts')
: t('storeItems.search.noMatch')}
</li>
) : (
filtered.map((option) => (
<li key={option.id}>
<button
type="button"
role="option"
aria-selected={option.id === value}
className={`${styles.option} ${option.id === value ? styles.optionSelected : ''}`}
onClick={() => selectOption(option.id)}
>
{preferFa && option.nameFa ? (
<>
<span className={styles.optionFa} dir="rtl">
{option.nameFa}
</span>
{option.title && (
<>
<span className={styles.optionSep}>/</span>
<span className={styles.optionEn} dir="rtl">
{option.title}
</span>
</>
)}
</>
) : (
<>
<span className={styles.optionEn} dir={preferFa ? 'rtl' : 'ltr'}>
{option.title}
</span>
{option.nameFa && (
<>
<span className={styles.optionSep}>/</span>
<span className={styles.optionFa} dir="rtl">
{option.nameFa}
</span>
</>
)}
</>
)}
</button>
</li>
))
)}
</ul>
)}
{open && query.trim().length > 0 && query.trim().length < minSearchLength && (
<ul className={`${styles.dropdown} ${styles.dropdownInFlow}`}>
<li className={styles.noResults}>
{t('storeItems.search.minChars', { count: minSearchLength })}
</li>
</ul>
)}
</div>
)
}