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, 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 {
|
||||
@@ -16,6 +18,14 @@ interface ProductSearchSelectProps {
|
||||
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({
|
||||
@@ -26,13 +36,18 @@ export function ProductSearchSelect({
|
||||
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()
|
||||
@@ -70,7 +85,7 @@ export function ProductSearchSelect({
|
||||
const showDropdown = open && query.trim().length >= minSearchLength
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} ref={containerRef}>
|
||||
<div className={styles.wrapper} ref={containerRef} dir={preferFa ? 'rtl' : 'ltr'}>
|
||||
<div className={`${styles.inputWrap} ${open ? styles.inputWrapOpen : ''}`}>
|
||||
<Search size={16} className={styles.searchIcon} />
|
||||
<input
|
||||
@@ -78,12 +93,8 @@ export function ProductSearchSelect({
|
||||
id={id}
|
||||
type="text"
|
||||
className={styles.input}
|
||||
placeholder={
|
||||
selected
|
||||
? `${selected.title}${selected.nameFa ? ` / ${selected.nameFa}` : ''}`
|
||||
: placeholder
|
||||
}
|
||||
value={open ? query : selected ? `${selected.title}${selected.nameFa ? ` / ${selected.nameFa}` : ''}` : ''}
|
||||
placeholder={selected ? selectedLabel : placeholder}
|
||||
value={open ? query : selectedLabel}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value)
|
||||
if (!open) setOpen(true)
|
||||
@@ -91,13 +102,15 @@ export function ProductSearchSelect({
|
||||
}}
|
||||
onFocus={handleFocus}
|
||||
disabled={disabled}
|
||||
aria-label={ariaLabel ?? placeholder}
|
||||
dir={preferFa ? 'rtl' : 'ltr'}
|
||||
/>
|
||||
{value && !open && (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearBtn}
|
||||
onClick={() => onChange('')}
|
||||
aria-label="Clear selection"
|
||||
aria-label={t('storeItems.search.clear')}
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
@@ -106,10 +119,12 @@ export function ProductSearchSelect({
|
||||
</div>
|
||||
|
||||
{showDropdown && (
|
||||
<ul className={styles.dropdown} role="listbox">
|
||||
<ul className={`${styles.dropdown} ${styles.dropdownInFlow}`} role="listbox">
|
||||
{filtered.length === 0 ? (
|
||||
<li className={styles.noResults}>
|
||||
{options.length === 0 ? 'No products available.' : 'No matching products.'}
|
||||
{options.length === 0
|
||||
? t('storeItems.search.noProducts')
|
||||
: t('storeItems.search.noMatch')}
|
||||
</li>
|
||||
) : (
|
||||
filtered.map((option) => (
|
||||
@@ -121,11 +136,33 @@ export function ProductSearchSelect({
|
||||
className={`${styles.option} ${option.id === value ? styles.optionSelected : ''}`}
|
||||
onClick={() => selectOption(option.id)}
|
||||
>
|
||||
<span className={styles.optionEn}>{option.title}</span>
|
||||
{option.nameFa && (
|
||||
{preferFa && option.nameFa ? (
|
||||
<>
|
||||
<span className={styles.optionSep}>/</span>
|
||||
<span className={styles.optionFa}>{option.nameFa}</span>
|
||||
<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>
|
||||
@@ -136,8 +173,10 @@ export function ProductSearchSelect({
|
||||
)}
|
||||
|
||||
{open && query.trim().length > 0 && query.trim().length < minSearchLength && (
|
||||
<ul className={styles.dropdown}>
|
||||
<li className={styles.noResults}>Type at least {minSearchLength} characters to search</li>
|
||||
<ul className={`${styles.dropdown} ${styles.dropdownInFlow}`}>
|
||||
<li className={styles.noResults}>
|
||||
{t('storeItems.search.minChars', { count: minSearchLength })}
|
||||
</li>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user