mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-12 06:40:57 +04:30
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.3 KiB
TypeScript
121 lines
3.3 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { isAbortError } from '../lib/api'
|
|
import { searchUsers } from '../services/userService'
|
|
import type { UserSearchItem } from '../types/user'
|
|
import styles from './UserSearchInput.module.css'
|
|
|
|
const DEFAULT_MIN_CHARS = 2
|
|
|
|
export interface UserSearchInputProps {
|
|
id?: string
|
|
label?: string
|
|
placeholder?: string
|
|
disabled?: boolean
|
|
minChars?: number
|
|
selectedUser: UserSearchItem | null
|
|
onSelect: (user: UserSearchItem | null) => void
|
|
}
|
|
|
|
export function UserSearchInput({
|
|
id = 'user-search',
|
|
label = 'User',
|
|
placeholder = 'Search by name, email, or phone',
|
|
disabled = false,
|
|
minChars = DEFAULT_MIN_CHARS,
|
|
selectedUser,
|
|
onSelect,
|
|
}: UserSearchInputProps) {
|
|
const [query, setQuery] = useState(selectedUser?.label ?? '')
|
|
const [results, setResults] = useState<UserSearchItem[]>([])
|
|
const [searching, setSearching] = useState(false)
|
|
|
|
useEffect(() => {
|
|
setQuery(selectedUser?.label ?? '')
|
|
}, [selectedUser])
|
|
|
|
useEffect(() => {
|
|
if (query.trim().length < minChars || selectedUser) {
|
|
setResults([])
|
|
setSearching(false)
|
|
return
|
|
}
|
|
|
|
const controller = new AbortController()
|
|
const timer = setTimeout(() => {
|
|
setSearching(true)
|
|
void searchUsers(query.trim(), 20, controller.signal)
|
|
.then((result) => {
|
|
if (!controller.signal.aborted) setResults(result.items)
|
|
})
|
|
.catch((err) => {
|
|
if (!isAbortError(err) && !controller.signal.aborted) {
|
|
setResults([])
|
|
}
|
|
})
|
|
.finally(() => {
|
|
if (!controller.signal.aborted) setSearching(false)
|
|
})
|
|
}, 300)
|
|
|
|
return () => {
|
|
clearTimeout(timer)
|
|
controller.abort()
|
|
}
|
|
}, [query, minChars, selectedUser])
|
|
|
|
const showDropdown = query.trim().length >= minChars && !selectedUser
|
|
|
|
function handleQueryChange(value: string) {
|
|
setQuery(value)
|
|
if (selectedUser && value !== selectedUser.label) {
|
|
onSelect(null)
|
|
}
|
|
}
|
|
|
|
function pickUser(user: UserSearchItem) {
|
|
onSelect(user)
|
|
setQuery(user.label)
|
|
setResults([])
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<label htmlFor={id}>{label}</label>
|
|
<div className={styles.wrap}>
|
|
<input
|
|
id={id}
|
|
value={query}
|
|
onChange={(e) => handleQueryChange(e.target.value)}
|
|
placeholder={placeholder}
|
|
autoComplete="off"
|
|
disabled={disabled}
|
|
/>
|
|
{showDropdown && (
|
|
<div className={styles.dropdown}>
|
|
{searching ? (
|
|
<div className={styles.empty}>Searching...</div>
|
|
) : results.length === 0 ? (
|
|
<div className={styles.empty}>No users found.</div>
|
|
) : (
|
|
results.map((user) => (
|
|
<button
|
|
key={user.id}
|
|
type="button"
|
|
className={styles.item}
|
|
onClick={() => pickUser(user)}
|
|
>
|
|
{user.label}
|
|
</button>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{query.trim().length > 0 && query.trim().length < minChars && (
|
|
<div className={styles.hint}>Type at least {minChars} characters to search.</div>
|
|
)}
|
|
{selectedUser && <div className={styles.selected}>Selected: {selectedUser.label}</div>}
|
|
</div>
|
|
)
|
|
}
|