feat: add ScriptFilterBar with category tabs and debounced search
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
75
frontend/src/components/scripts/ScriptFilterBar.tsx
Normal file
75
frontend/src/components/scripts/ScriptFilterBar.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
import { Search } from 'lucide-react'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useScriptGeneratorStore } from '@/store/scriptGeneratorStore'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
inputValue: string
|
||||||
|
setInputValue: (value: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScriptFilterBar({ inputValue, setInputValue }: Props) {
|
||||||
|
const categories = useScriptGeneratorStore(s => s.categories)
|
||||||
|
const activeCategoryId = useScriptGeneratorStore(s => s.activeCategoryId)
|
||||||
|
const setCategory = useScriptGeneratorStore(s => s.setCategory)
|
||||||
|
const setSearch = useScriptGeneratorStore(s => s.setSearch)
|
||||||
|
|
||||||
|
// Debounce: 300ms after the input value settles, push to store
|
||||||
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
setSearch(inputValue)
|
||||||
|
}, 300)
|
||||||
|
return () => {
|
||||||
|
if (debounceRef.current) clearTimeout(debounceRef.current)
|
||||||
|
}
|
||||||
|
}, [inputValue, setSearch])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
{/* Category pills */}
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCategory(null)}
|
||||||
|
className={cn(
|
||||||
|
'font-label text-xs px-3 py-1.5 rounded-full border transition-all',
|
||||||
|
activeCategoryId === null
|
||||||
|
? 'bg-primary/10 border-primary/30 text-foreground border-l-[3px] border-l-primary'
|
||||||
|
: 'border-border text-muted-foreground hover:border-white/12 hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{categories.map(cat => (
|
||||||
|
<button
|
||||||
|
key={cat.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setCategory(cat.id)}
|
||||||
|
className={cn(
|
||||||
|
'font-label text-xs px-3 py-1.5 rounded-full border transition-all',
|
||||||
|
activeCategoryId === cat.id
|
||||||
|
? 'bg-primary/10 border-primary/30 text-foreground border-l-[3px] border-l-primary'
|
||||||
|
: 'border-border text-muted-foreground hover:border-white/12 hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{cat.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Search input */}
|
||||||
|
<div className="relative ml-auto">
|
||||||
|
<Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={e => setInputValue(e.target.value)}
|
||||||
|
placeholder="Search templates…"
|
||||||
|
className="pl-8 pr-3 py-1.5 text-sm rounded-md border border-border bg-card text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-[rgba(6,182,212,0.3)] focus:ring-1 focus:ring-[rgba(6,182,212,0.2)] w-52"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user