Files
jellybloom/src/pages/library/controls.tsx
T
2026-04-01 08:12:28 +03:00

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>
)
}