Scrum4Me/components/shared/backlog-filter-popover.tsx
Madhura68 6fe1a2aaa6 feat: shared backlog filter popover + sprint header polish (v1.3.3)
- Move sprint switcher into sprint header, centered between title and actions
- Extract BacklogFilterPopover as shared component used by sprint and product backlog
- Add sort options (code/priority/status) with single-pill asc/desc toggle
- Default sprint backlog status filter to OPEN, remove "alleen niet klaar" button
- Persist collapsed state and filter popover open in localStorage
- Fix hydration flicker: defer localStorage read to useEffect with prefsLoaded gate for writes
- Increase sprint switcher text size for readability

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:11:02 +02:00

186 lines
5.4 KiB
TypeScript

'use client'
import { ArrowDown, ArrowUp } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { debugProps } from '@/lib/debug'
export const PRIORITY_LABELS: Record<number, string> = {
1: 'Kritiek',
2: 'Hoog',
3: 'Gemiddeld',
4: 'Laag',
}
export const PRIORITY_OPTIONS: Array<{ value: number | 'all'; label: string }> = [
{ value: 'all', label: 'Alle' },
{ value: 1, label: 'Kritiek' },
{ value: 2, label: 'Hoog' },
{ value: 3, label: 'Gemiddeld' },
{ value: 4, label: 'Laag' },
]
export type SortDir = 'asc' | 'desc'
export function FilterPills<T extends string | number>({
label,
options,
value,
onChange,
}: {
label: string
options: Array<{ value: T; label: string }>
value: T
onChange: (v: T) => void
}) {
return (
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">{label}</p>
<div className="flex flex-wrap gap-1.5">
{options.map((opt) => (
<button
key={String(opt.value)}
type="button"
onClick={() => onChange(opt.value)}
className={cn(
'text-xs px-2.5 py-1 rounded-full border transition-colors',
value === opt.value
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent border-border hover:bg-surface-container'
)}
>
{opt.label}
</button>
))}
</div>
</div>
)
}
interface BacklogFilterPopoverProps<S extends string, So extends string> {
open: boolean
onOpenChange: (open: boolean) => void
filterPriority: number | 'all'
onFilterPriorityChange: (v: number | 'all') => void
filterStatus: S
onFilterStatusChange: (v: S) => void
statusOptions: Array<{ value: S; label: string }>
sort: So
onSortChange: (v: So) => void
sortDir: SortDir
onSortDirChange: (v: SortDir) => void
sortOptions: Array<{ value: So; label: string }>
activeFilterCount: number
onReset: () => void
resetDisabled: boolean
}
export function BacklogFilterPopover<S extends string, So extends string>({
open,
onOpenChange,
filterPriority,
onFilterPriorityChange,
filterStatus,
onFilterStatusChange,
statusOptions,
sort,
onSortChange,
sortDir,
onSortDirChange,
sortOptions,
activeFilterCount,
onReset,
resetDisabled,
}: BacklogFilterPopoverProps<S, So>) {
return (
<Popover open={open} onOpenChange={onOpenChange}>
<PopoverTrigger
render={
<Button
variant="outline"
size="sm"
className="h-7 text-xs"
{...debugProps('backlog-filter-popover__trigger')}
>
{`Filters${activeFilterCount > 0 ? ` (${activeFilterCount})` : ''}`}
</Button>
}
/>
<PopoverContent
align="end"
className="w-72 space-y-4"
{...debugProps('backlog-filter-popover', 'BacklogFilterPopover', 'components/shared/backlog-filter-popover.tsx')}
>
<FilterPills
label="Prioriteit"
options={PRIORITY_OPTIONS}
value={filterPriority}
onChange={onFilterPriorityChange}
/>
<FilterPills
label="Status"
options={statusOptions}
value={filterStatus}
onChange={onFilterStatusChange}
/>
<div className="space-y-1.5">
<p className="text-xs font-medium text-muted-foreground">Sorteren op</p>
<div className="flex flex-wrap gap-1.5">
{sortOptions.map((opt) => {
const active = sort === opt.value
return (
<button
key={opt.value}
type="button"
onClick={() => {
if (active) {
onSortDirChange(sortDir === 'asc' ? 'desc' : 'asc')
} else {
onSortChange(opt.value)
onSortDirChange('asc')
}
}}
aria-label={
active
? `Sorteren op ${opt.label} ${sortDir === 'asc' ? 'oplopend' : 'aflopend'} — klik om te wisselen`
: `Sorteren op ${opt.label}`
}
className={cn(
'inline-flex items-center gap-1 text-xs px-2.5 py-1 rounded-full border transition-colors',
active
? 'bg-primary text-primary-foreground border-primary'
: 'bg-transparent border-border hover:bg-surface-container'
)}
>
<span>{opt.label}</span>
{active && (
sortDir === 'asc'
? <ArrowDown size={12} aria-hidden />
: <ArrowUp size={12} aria-hidden />
)}
</button>
)
})}
</div>
</div>
<div className="flex justify-end pt-1 border-t border-border">
<Button
type="button"
variant="ghost"
size="sm"
className="h-7 text-xs"
disabled={resetDisabled}
onClick={onReset}
>
Wis filters
</Button>
</div>
</PopoverContent>
</Popover>
)
}