feat: shared backlog filter popover + sprint header polish (v1.3.3) (#184)
- 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>
This commit is contained in:
parent
a9b53dedf0
commit
1f8cbacb0a
9 changed files with 424 additions and 330 deletions
186
components/shared/backlog-filter-popover.tsx
Normal file
186
components/shared/backlog-filter-popover.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
'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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue