feat: ST-601-ST-612 M6 polish, beveiliging en launch-ready

- ST-601/602: loading skeletons en error boundary
- ST-603: Sonner toasts op alle CRUD-operaties
- ST-604: DemoTooltip op uitgeschakelde knoppen
- ST-605: KeyboardSensor dnd-kit, Escape sluit modals
- ST-606: min-width banner < 1024px
- ST-607: WCAG AA aria-labels en skip link
- ST-608: rate limiting login (10/min) en registratie (5/uur)
- ST-609: security integratietests cross-user toegang (7 tests)
- ST-610: GitHub Actions CI/CD workflow
- ST-611: README met quickstart, deployment en API-docs
- ST-612: Lars-flow acceptatiechecklist
- fix: settings toont gebruikersnaam i.p.v. interne id
- fix: seed idempotent, testdata altijd gekoppeld aan demo-gebruiker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-24 12:36:23 +02:00
parent 8bb8754d01
commit d11b114fc1
27 changed files with 1858 additions and 67 deletions

View file

@ -4,10 +4,11 @@ import { useState, useTransition, useEffect, useActionState } from 'react'
import { useFormStatus } from 'react-dom'
import {
DndContext, DragEndEvent, DragOverlay, DragStartEvent,
PointerSensor, useSensor, useSensors, closestCenter,
KeyboardSensor, PointerSensor, useSensor, useSensors, closestCenter,
} from '@dnd-kit/core'
import {
SortableContext, useSortable, verticalListSortingStrategy, arrayMove,
sortableKeyboardCoordinates,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { toast } from 'sonner'
@ -97,7 +98,7 @@ function SortableTaskRow({
</p>
<span className="text-xs text-muted-foreground">{PRIORITY_LABELS[task.priority]}</span>
</div>
<button onClick={onStatusToggle} disabled={isDemo}>
<button onClick={onStatusToggle} disabled={isDemo} aria-label={`Status: ${STATUS_LABELS[task.status]}`}>
<Badge className={cn('text-xs border cursor-pointer hover:opacity-80 transition-opacity', STATUS_COLORS[task.status])}>
{STATUS_LABELS[task.status]}
</Badge>
@ -105,7 +106,7 @@ function SortableTaskRow({
{!isDemo && (
<div className="opacity-0 group-hover:opacity-100 flex gap-1">
<button onClick={() => setEditing(true)} className="text-xs text-muted-foreground hover:text-foreground">Bewerk</button>
<button onClick={onDelete} className="text-xs text-muted-foreground hover:text-error">×</button>
<button onClick={onDelete} aria-label="Verwijder taak" className="text-xs text-muted-foreground hover:text-error">×</button>
</div>
)}
</div>
@ -161,7 +162,10 @@ export function TaskList({ storyId, sprintId, productId, tasks, isDemo }: TaskLi
const doneCount = orderedTasks.filter(t => t.status === 'DONE').length
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 5 } }))
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
)
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
@ -184,7 +188,8 @@ export function TaskList({ storyId, sprintId, productId, tasks, isDemo }: TaskLi
function handleDelete(id: string) {
startTransition(async () => {
await deleteTaskAction(id)
const result = await deleteTaskAction(id)
if (result && 'error' in result) toast.error(result.error ?? 'Verwijderen mislukt')
})
}