Add custom color picker, move website link to store items, and fix sidebar logo aspect.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
592540cc7e
commit
00f2e0f729
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ChevronDown, Search, X } from 'lucide-react'
|
||||
import { ChevronDown, Loader2, Search, X } from 'lucide-react'
|
||||
import { useLocale } from '@meshkee/dashboard-ui'
|
||||
import { useT } from '../i18n/useT'
|
||||
import styles from './SearchableSelect.module.css'
|
||||
|
||||
export const PRODUCT_SEARCH_DEBOUNCE_MS = 350
|
||||
|
||||
export interface ProductSearchOption {
|
||||
id: string
|
||||
title: string
|
||||
@@ -18,6 +20,13 @@ interface ProductSearchSelectProps {
|
||||
disabled?: boolean
|
||||
id?: string
|
||||
minSearchLength?: number
|
||||
isSearching?: boolean
|
||||
/**
|
||||
* When set, typing is debounced and the parent fetches results.
|
||||
* Local filtering is skipped.
|
||||
*/
|
||||
onQueryChange?: (query: string) => void
|
||||
debounceMs?: number
|
||||
'aria-label'?: string
|
||||
}
|
||||
|
||||
@@ -36,6 +45,9 @@ export function ProductSearchSelect({
|
||||
disabled = false,
|
||||
id,
|
||||
minSearchLength = 3,
|
||||
isSearching = false,
|
||||
onQueryChange,
|
||||
debounceMs = PRODUCT_SEARCH_DEBOUNCE_MS,
|
||||
'aria-label': ariaLabel,
|
||||
}: ProductSearchSelectProps) {
|
||||
const t = useT()
|
||||
@@ -45,20 +57,35 @@ export function ProductSearchSelect({
|
||||
const [query, setQuery] = useState('')
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
const onQueryChangeRef = useRef(onQueryChange)
|
||||
onQueryChangeRef.current = onQueryChange
|
||||
|
||||
const selected = options.find((option) => option.id === value)
|
||||
const selectedLabel = selected ? formatProductLabel(selected, preferFa) : ''
|
||||
const trimmedQuery = query.trim()
|
||||
const canSearch = trimmedQuery.length >= minSearchLength
|
||||
|
||||
useEffect(() => {
|
||||
const cb = onQueryChangeRef.current
|
||||
if (!cb) return
|
||||
if (!canSearch) {
|
||||
cb('')
|
||||
return
|
||||
}
|
||||
const timer = window.setTimeout(() => cb(trimmedQuery), debounceMs)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [canSearch, trimmedQuery, debounceMs])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const trimmed = query.trim()
|
||||
if (trimmed.length < minSearchLength) return []
|
||||
const q = trimmed.toLowerCase()
|
||||
if (!canSearch) return []
|
||||
if (onQueryChange) return options
|
||||
const q = trimmedQuery.toLowerCase()
|
||||
return options.filter(
|
||||
(option) =>
|
||||
option.title.toLowerCase().includes(q) ||
|
||||
option.nameFa.includes(trimmed),
|
||||
option.nameFa.includes(trimmedQuery),
|
||||
)
|
||||
}, [options, query, minSearchLength])
|
||||
}, [canSearch, onQueryChange, options, trimmedQuery])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
@@ -76,13 +103,14 @@ export function ProductSearchSelect({
|
||||
onChange(id)
|
||||
setOpen(false)
|
||||
setQuery('')
|
||||
onQueryChangeRef.current?.('')
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
if (!disabled) setOpen(true)
|
||||
}
|
||||
|
||||
const showDropdown = open && query.trim().length >= minSearchLength
|
||||
const showDropdown = open && (canSearch || trimmedQuery.length > 0)
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper} ref={containerRef} dir={preferFa ? 'rtl' : 'ltr'}>
|
||||
@@ -115,17 +143,19 @@ export function ProductSearchSelect({
|
||||
<X size={14} />
|
||||
</button>
|
||||
)}
|
||||
<ChevronDown size={16} className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`} />
|
||||
{isSearching && open ? (
|
||||
<Loader2 size={16} className={`${styles.chevron} ${styles.spin}`} />
|
||||
) : (
|
||||
<ChevronDown size={16} className={`${styles.chevron} ${open ? styles.chevronOpen : ''}`} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showDropdown && (
|
||||
{showDropdown && canSearch && (
|
||||
<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>
|
||||
{isSearching && filtered.length === 0 ? (
|
||||
<li className={styles.noResults}>{t('storeItems.search.searching')}</li>
|
||||
) : filtered.length === 0 ? (
|
||||
<li className={styles.noResults}>{t('storeItems.search.noMatch')}</li>
|
||||
) : (
|
||||
filtered.map((option) => (
|
||||
<li key={option.id}>
|
||||
@@ -172,7 +202,7 @@ export function ProductSearchSelect({
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{open && query.trim().length > 0 && query.trim().length < minSearchLength && (
|
||||
{open && trimmedQuery.length > 0 && trimmedQuery.length < minSearchLength && (
|
||||
<ul className={`${styles.dropdown} ${styles.dropdownInFlow}`}>
|
||||
<li className={styles.noResults}>
|
||||
{t('storeItems.search.minChars', { count: minSearchLength })}
|
||||
|
||||
Reference in New Issue
Block a user