refactor: extract BacklogCard component, use for stories and PBIs

Single card component with priority border, selected state, badge
and actions slots. Replaces duplicate inline card markup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-26 18:13:03 +02:00
parent 6671f3118c
commit 34848588a8
3 changed files with 85 additions and 54 deletions

View file

@ -31,6 +31,7 @@ import { deletePbiAction } from '@/actions/pbis'
import { reorderPbisAction, updatePbiPriorityAction } from '@/actions/stories'
import { cn } from '@/lib/utils'
import { PbiDialog, type PbiDialogState } from './pbi-dialog'
import { BacklogCard } from './backlog-card'
const PRIORITY_LABELS: Record<number, string> = {
1: 'Kritiek',
@ -82,37 +83,25 @@ function SortablePbiRow({
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.4 : 1,
}
return (
<div
<BacklogCard
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
title={pbi.title}
priority={pbi.priority}
isSelected={isSelected}
isDragging={isDragging}
role="button"
tabIndex={0}
aria-selected={isSelected}
onClick={onSelect}
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect() } }}
className={cn(
'group flex items-center justify-between px-4 py-2 cursor-pointer transition-colors hover:bg-surface-container focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
isSelected && 'bg-primary-container text-primary-container-foreground'
)}
>
{!isDemo && (
<span
{...attributes}
{...listeners}
aria-label="Versleep om te sorteren"
className="mr-2 text-muted-foreground cursor-grab active:cursor-grabbing shrink-0 select-none"
onClick={(e) => e.stopPropagation()}
>
</span>
)}
<span className="text-sm truncate flex-1">{pbi.title}</span>
{!isDemo && (
<div className="flex items-center gap-1 ml-2 shrink-0">
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onSelect() } }}
actions={!isDemo ? (
<div className="flex items-center gap-1">
<button
onClick={(e) => { e.stopPropagation(); onEdit() }}
className="border border-border rounded px-1.5 py-0.5 text-xs text-muted-foreground hover:text-foreground hover:bg-surface-container transition-colors"
@ -122,14 +111,14 @@ function SortablePbiRow({
</button>
<button
onClick={(e) => { e.stopPropagation(); onDelete() }}
className="opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-error text-xs"
className="text-muted-foreground hover:text-error text-xs"
aria-label="Verwijder PBI"
>
×
</button>
</div>
)}
</div>
) : undefined}
/>
)
}
@ -286,10 +275,10 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<div className="py-2">
<div className="p-3 space-y-4">
{visiblePriorities.map(priority => (
<div key={priority}>
<div className="flex items-center gap-2 px-4 py-1.5">
<div className="flex items-center gap-2 mb-2">
<span className={cn('text-xs font-semibold px-2 py-0.5 rounded-full border', PRIORITY_COLORS[priority])}>
{PRIORITY_LABELS[priority]}
</span>
@ -309,6 +298,7 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
items={grouped[priority].map(p => p.id)}
strategy={verticalListSortingStrategy}
>
<div className="flex flex-col gap-2">
{grouped[priority].map(pbi => (
<SortablePbiRow
key={pbi.id}
@ -320,17 +310,19 @@ export function PbiList({ productId, pbis, isDemo }: PbiListProps) {
onDelete={() => handleDelete(pbi.id)}
/>
))}
</div>
</SortableContext>
</div>
))}
</div>
<DragOverlay>
{activePbi && (
<div className="bg-surface-container-low border border-primary rounded px-4 py-2 text-sm shadow-lg opacity-90">
{activePbi.title}
</div>
<BacklogCard
title={activePbi.title}
priority={activePbi.priority}
className="border-primary shadow-xl opacity-90"
/>
)}
</DragOverlay>
</DndContext>