Scrum4Me/components/notifications/answer-modal.tsx
Janpeter Visser d292e445d9
Sprint: Verbeteren debug mode (#179)
* feat(PBI-49): add debugProps helper + Vitest test

Adds lib/debug.ts with debugProps(id, component, file) that returns
data-debug-id and data-debug-label attrs in dev mode, empty object in
production. Adds __tests__/lib/debug.test.ts covering both modes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(PBI-49): add debug-id pattern doc + CLAUDE.md reference

Adds docs/patterns/debug-id.md documenting the named-component boundary
rule (6 punten), helper-voorbeeld, skip-criteria en motivatie voor
handmatige pad-argumenten. Voegt verwijzing toe aan CLAUDE.md
patterns-tabel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): migrate 17 shared/ components to debugProps helper

Replace hardcoded data-debug-id + data-debug-label attribute pairs with
{...debugProps(id, component, file)} spread in all 17 components/shared/
files. Existing debug-ids preserved unchanged.

* feat(PBI-49): add debugProps to backlog/, sprint/, solo/ components

* feat(PBI-49): add debugProps to jobs/ + ideas/ components

* feat(PBI-49): add debugProps to products/ + settings/ + notifications/ components

* feat(PBI-49): add debugProps to admin/ + dashboard/ + dialogs/ + mobile/ + split-pane/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(PBI-49): use attr(data-debug-id) for debug tooltip in globals.css

* refactor(PBI-49): remove data-debug-label from debugProps helper + test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(PBI-49): strip unused component/file args from debugProps in shared/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to StatusBar, NavBar, PanelNavBar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/sprint/*

- new-sprint-dialog: __submit on submit button
- sprint-backlog: __list on SprintBacklogLeft + SprintBacklogRight scroll areas
- sprint-board-client: root wrapper div (display:contents) + __drag-overlay
- sprint-header: __title on goal button, __dates on dates button, __actions on action cluster
- sprint-run-controls: root on controls div, __start/__cancel on action buttons; __blockers-dialog on dialog content
- start-sprint-button: root on trigger button, __dialog on dialog content, __submit on submit button
- sync-active-sprint-cookie: no debug-id (returns null, side-effect only), comment added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/backlog/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/ideas/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/dashboard/* + components/markdown.tsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to new-product-button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/solo/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-elements to nav-status-indicators

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/jobs/*

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to components/products/*

* feat(PBI-49): add BEM sub-element data-debug-id to components/notifications/*

- answer-modal: __content (scroll area), __submit (footer)
- notifications-bridge: skip comment (bridge, non-rendering wrapper)
- notifications-realtime-mount: skip comment (returns null)
- notifications-sheet: __header, __items (questions list)
- push-toggle: __switch (button), __label (button text) on subscribed/unsubscribed states

* feat(PBI-49): add BEM sub-element data-debug-id to components/settings/*

- leave-product-button: root only (single-button component)
- min-quota-editor: __input (number input), __save (save button)
- profile-editor: __username (bio/short-description input), __save (submit)
- role-manager: __roles (checkbox list), __add (save button)
- token-manager: __tokens (active tokens list), __generate (create button)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(PBI-49): add BEM sub-element data-debug-id to admin, auth, dialogs, entity-dialog, mobile, split-pane

* docs(PBI-49): add debug-labels BEM pattern doc + CLAUDE.md entry

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:46:29 +02:00

183 lines
6.5 KiB
TypeScript

'use client'
// ST-1105: Modal waar de gebruiker een Claude-vraag beantwoordt (M11).
//
// Free-text Textarea (max ANSWER_MAX_CHARS) of multiple-choice via knoppen
// wanneer de vraag `options` heeft. Submit roept answerQuestion-Server-Action
// aan via useTransition; bij succes wordt de vraag uit de store verwijderd
// (optimistisch) en sluit de modal. Demo-modus: textarea readOnly + submit
// disabled met DemoTooltip.
import { useState, useTransition } from 'react'
import Link from 'next/link'
import { ExternalLink } from 'lucide-react'
import { toast } from 'sonner'
import {
Dialog,
DialogContent,
DialogDescription,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { DemoTooltip } from '@/components/shared/demo-tooltip'
import {
useDirtyCloseGuard,
DirtyCloseGuardDialog,
} from '@/components/shared/use-dirty-close-guard'
import { useDialogSubmitShortcut } from '@/components/shared/use-dialog-submit-shortcut'
import {
entityDialogContentClasses,
entityDialogFooterClasses,
entityDialogHeaderClasses,
} from '@/components/shared/entity-dialog-layout'
import { ANSWER_MAX_CHARS } from '@/lib/schemas/question-answer'
import { answerQuestion } from '@/actions/questions'
import { useNotificationsStore, type NotificationQuestion } from '@/stores/notifications-store'
import { debugProps } from '@/lib/debug'
interface AnswerModalProps {
question: NotificationQuestion | null
isDemo: boolean
onClose: () => void
}
export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) {
const [answer, setAnswer] = useState('')
const [pending, startTransition] = useTransition()
const closeGuard = useDirtyCloseGuard(answer.trim().length > 0, () => {
setAnswer('')
onClose()
})
const charsLeft = ANSWER_MAX_CHARS - answer.length
const tooLong = charsLeft < 0
const submitDisabled = isDemo || pending || answer.trim().length === 0 || tooLong
function submit(text: string) {
if (!question) return
startTransition(async () => {
const res = await answerQuestion(question.id, text)
if (!res.ok) {
toast.error(res.error)
return
}
useNotificationsStore.getState().remove(question.id)
toast.success('Antwoord verstuurd')
setAnswer('')
onClose()
})
}
const handleKeyDown = useDialogSubmitShortcut(() => {
if (!submitDisabled) submit(answer)
})
if (!question) return null
return (
<>
<Dialog open={!!question} onOpenChange={(open) => { if (!open) closeGuard.attemptClose() }}>
<DialogContent
showCloseButton={false}
onKeyDown={handleKeyDown}
className={entityDialogContentClasses}
{...debugProps('answer-modal', 'AnswerModal', 'components/notifications/answer-modal.tsx')}
>
<div className={entityDialogHeaderClasses}>
<div className="flex flex-col gap-1">
<DialogTitle className="text-xl font-semibold">Beantwoord Claude</DialogTitle>
<DialogDescription>
<span className="font-mono">
{question.kind === 'idea' ? question.idea_code : (question.story_code ?? 'story')}
</span>
{' — '}
{question.kind === 'idea' ? question.idea_title : question.story_title}
</DialogDescription>
</div>
</div>
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6" data-debug-id="answer-modal__content">
<Link
href={
question.kind === 'idea'
? `/ideas/${question.idea_id}?tab=timeline`
: `/products/${question.product_id}/sprint`
}
className="text-primary inline-flex items-center gap-1 self-start text-xs hover:underline"
>
<ExternalLink className="h-3.5 w-3.5" />
<span>{question.kind === 'idea' ? 'Open idee' : 'Open in Sprint'}</span>
</Link>
<div className="bg-surface-container-low rounded-md border p-3 text-sm whitespace-pre-wrap">
{question.question}
</div>
{question.options && question.options.length > 0 ? (
<div className="space-y-2">
<p className="text-muted-foreground text-xs">Kies een van de opties:</p>
<div className="flex flex-col gap-2">
{question.options.map((opt) => (
<DemoTooltip key={opt} show={isDemo}>
<Button
type="button"
variant="outline"
className="justify-start"
disabled={isDemo || pending}
onClick={() => submit(opt)}
>
{opt}
</Button>
</DemoTooltip>
))}
</div>
</div>
) : (
<div className="space-y-1">
<Textarea
value={answer}
onChange={(e) => setAnswer(e.target.value)}
placeholder="Typ je antwoord…"
rows={5}
maxLength={ANSWER_MAX_CHARS}
disabled={isDemo}
aria-label="Antwoord op Claude's vraag"
/>
<div
className={
tooLong
? 'text-error text-right text-xs'
: 'text-muted-foreground text-right text-xs'
}
>
{charsLeft} tekens over
</div>
</div>
)}
</div>
<div className={entityDialogFooterClasses} data-debug-id="answer-modal__submit">
<div className="flex justify-end gap-2">
<Button variant="ghost" onClick={closeGuard.attemptClose} disabled={pending}>
Annuleren
</Button>
{(!question.options || question.options.length === 0) && (
<DemoTooltip show={isDemo}>
<Button
onClick={() => submit(answer)}
disabled={submitDisabled}
>
{pending ? 'Bezig…' : 'Verstuur'}
</Button>
</DemoTooltip>
)}
</div>
</div>
</DialogContent>
</Dialog>
<DirtyCloseGuardDialog guard={closeGuard} />
</>
)
}