113 lines
3.9 KiB
TypeScript
113 lines
3.9 KiB
TypeScript
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>
|
|
)
|
|
}
|