library controls and filters
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Bookmark, Trash2 } from '../../lib/icons'
|
||||
import { useSavedSearches, type SavedSearchFilters } from '../../stores/saved-searches-store'
|
||||
|
||||
export function WatchedPills({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: 'any' | 'played' | 'unplayed'
|
||||
onChange: (v: 'any' | 'played' | 'unplayed') => void
|
||||
}) {
|
||||
const opts: Array<{ key: 'any' | 'played' | 'unplayed'; label: string }> = [
|
||||
{ key: 'any', label: 'All' },
|
||||
{ key: 'unplayed', label: 'Unwatched' },
|
||||
{ key: 'played', label: 'Watched' },
|
||||
]
|
||||
return (
|
||||
<div className="inline-flex items-center bg-elevated/50 border border-border rounded-md p-0.5">
|
||||
{opts.map(o => {
|
||||
const on = value === o.key
|
||||
return (
|
||||
<button
|
||||
key={o.key}
|
||||
onClick={() => onChange(o.key)}
|
||||
className={`h-6 px-2.5 rounded text-[11px] font-medium tracking-tight transition focus-ring ${
|
||||
on ? 'bg-accent text-void' : 'text-text-3 hover:text-text-1'
|
||||
}`}
|
||||
>
|
||||
{o.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function SavedSearchMenu({
|
||||
scope,
|
||||
onApply,
|
||||
onSave,
|
||||
}: {
|
||||
scope: 'movies' | 'shows'
|
||||
onApply: (filters: SavedSearchFilters) => void
|
||||
onSave: () => void
|
||||
}) {
|
||||
// Select the whole array; filter in render. Returning a fresh array from
|
||||
// the selector breaks Zustand's snapshot identity check and sends
|
||||
// useSyncExternalStore into an update loop.
|
||||
const allSearches = useSavedSearches(s => s.searches)
|
||||
const searches = useMemo(
|
||||
() => allSearches.filter(x => x.scope === scope),
|
||||
[allSearches, scope],
|
||||
)
|
||||
const remove = useSavedSearches(s => s.remove)
|
||||
const [open, setOpen] = useState(false)
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setOpen(v => !v)}
|
||||
className="inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md text-[11.5px] font-medium tracking-tight bg-elevated/50 text-text-2 ring-1 ring-border hover:text-text-1 transition focus-ring"
|
||||
>
|
||||
<Bookmark size={11} stroke={2} />
|
||||
Saved
|
||||
{searches.length > 0 && (
|
||||
<span className="text-text-4 tabular-nums">{searches.length}</span>
|
||||
)}
|
||||
</button>
|
||||
{open && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-30" onClick={() => setOpen(false)} />
|
||||
<div className="absolute left-0 top-full mt-1.5 z-40 min-w-[220px] rounded-xl bg-surface ring-1 ring-border-strong shadow-xl overflow-hidden">
|
||||
<button
|
||||
onClick={() => {
|
||||
onSave()
|
||||
setOpen(false)
|
||||
}}
|
||||
className="w-full text-left px-3 py-2 text-[12px] hover:bg-elevated/80 text-accent font-medium transition border-b border-border"
|
||||
>
|
||||
Save current filters as...
|
||||
</button>
|
||||
{searches.length === 0 ? (
|
||||
<p className="px-3 py-3 text-[11.5px] text-text-4 text-center">
|
||||
No saved searches yet.
|
||||
</p>
|
||||
) : (
|
||||
searches.map(s => (
|
||||
<div key={s.id} className="flex items-center group">
|
||||
<button
|
||||
onClick={() => {
|
||||
onApply(s.filters)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="flex-1 text-left px-3 py-2 text-[12px] hover:bg-elevated/80 text-text-2 transition truncate"
|
||||
>
|
||||
{s.name}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => remove(s.id)}
|
||||
aria-label="Delete saved search"
|
||||
className="px-2.5 py-2 text-text-4 hover:text-red-300 transition opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<Trash2 size={11} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user