Initial Switch to V2. Completely Overhauled Backend, Frontend and General Structure.
This commit is contained in:
101
frontend/src/components/ui/SearchBar.jsx
Normal file
101
frontend/src/components/ui/SearchBar.jsx
Normal file
@@ -0,0 +1,101 @@
|
||||
// src/components/ui/SearchBar.jsx
|
||||
// Debounced search input with a magnifier icon and animated clear button.
|
||||
//
|
||||
// Props:
|
||||
// value — string — controlled value (optional; uncontrolled if omitted)
|
||||
// onChange — (value: string) => void — called after debounce delay
|
||||
// placeholder — string (default: 'Search…')
|
||||
// debounce — number — ms delay before onChange fires (default: 300)
|
||||
// disabled — boolean
|
||||
// className — extra classes on the wrapper
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
|
||||
export default function SearchBar({
|
||||
value: controlledValue,
|
||||
onChange,
|
||||
placeholder = 'Search…',
|
||||
debounce = 300,
|
||||
disabled = false,
|
||||
className = '',
|
||||
}) {
|
||||
const isControlled = controlledValue !== undefined
|
||||
const [localValue, setLocalValue] = useState(isControlled ? controlledValue : '')
|
||||
const timerRef = useRef(null)
|
||||
const inputRef = useRef(null)
|
||||
const prevControlledRef = useRef(controlledValue)
|
||||
|
||||
// Only sync from outside when the parent *explicitly* changes the value
|
||||
// (e.g. "Clear filters" resets it to ''). Do NOT sync on every re-render —
|
||||
// that's what caused the keystroke lag: the debounced parent setState was
|
||||
// writing back into this input and overwriting what the user just typed.
|
||||
useEffect(() => {
|
||||
if (isControlled && controlledValue !== prevControlledRef.current) {
|
||||
prevControlledRef.current = controlledValue
|
||||
setLocalValue(controlledValue)
|
||||
}
|
||||
}, [controlledValue, isControlled])
|
||||
|
||||
function handleChange(e) {
|
||||
const v = e.target.value
|
||||
setLocalValue(v)
|
||||
prevControlledRef.current = v // keep ref in sync so the effect doesn't clobber it
|
||||
if (debounce > 0) {
|
||||
clearTimeout(timerRef.current)
|
||||
timerRef.current = setTimeout(() => onChange?.(v), debounce)
|
||||
} else {
|
||||
onChange?.(v)
|
||||
}
|
||||
}
|
||||
|
||||
function handleClear() {
|
||||
setLocalValue('')
|
||||
prevControlledRef.current = ''
|
||||
clearTimeout(timerRef.current)
|
||||
onChange?.('')
|
||||
inputRef.current?.focus()
|
||||
}
|
||||
|
||||
const displayValue = localValue
|
||||
|
||||
return (
|
||||
<div className={`searchbar ${className}`}>
|
||||
{/* Search icon */}
|
||||
<span className="searchbar-icon" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="6.5" cy="6.5" r="4.5" />
|
||||
<path d="M10 10l3.5 3.5" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
className="searchbar-input"
|
||||
value={displayValue}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
aria-label={placeholder}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
|
||||
{/* Clear button — only when there's text */}
|
||||
{displayValue && !disabled && (
|
||||
<button
|
||||
type="button"
|
||||
className="searchbar-clear"
|
||||
onClick={handleClear}
|
||||
aria-label="Clear search"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
|
||||
<path d="M1 1l8 8M9 1L1 9" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user