feat: implement full admin panel with dashboard, user management, and platform settings
Adds complete super_admin panel with 9 pages and account owner categories page. Backend includes 5 new DB tables, ~25 API endpoints, settings manager with in-memory cache, and 29 integration tests. Frontend includes reusable admin components (DataTable, Pagination, ActionMenu, etc.) with code-split lazy loading. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
66
frontend/src/components/admin/SearchInput.tsx
Normal file
66
frontend/src/components/admin/SearchInput.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Search, X } from 'lucide-react'
|
||||
import { debounce } from 'lodash'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface SearchInputProps {
|
||||
value?: string
|
||||
onSearch: (value: string) => void
|
||||
placeholder?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SearchInput({ value = '', onSearch, placeholder = 'Search...', className }: SearchInputProps) {
|
||||
const [localValue, setLocalValue] = useState(value)
|
||||
const debouncedRef = useRef<ReturnType<typeof debounce> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalValue(value)
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
debouncedRef.current = debounce((v: string) => {
|
||||
onSearch(v)
|
||||
}, 300)
|
||||
return () => {
|
||||
debouncedRef.current?.cancel()
|
||||
}
|
||||
}, [onSearch])
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const v = e.target.value
|
||||
setLocalValue(v)
|
||||
debouncedRef.current?.(v)
|
||||
}, [])
|
||||
|
||||
const handleClear = () => {
|
||||
setLocalValue('')
|
||||
onSearch('')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
className={cn(
|
||||
'h-9 w-full rounded-md border border-border bg-background pl-9 pr-8 text-sm',
|
||||
'placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring'
|
||||
)}
|
||||
/>
|
||||
{localValue && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchInput
|
||||
Reference in New Issue
Block a user