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