diff --git a/actions/questions.ts b/actions/questions.ts index 19a45bc..a14af59 100644 --- a/actions/questions.ts +++ b/actions/questions.ts @@ -12,15 +12,10 @@ // realtime updates voor andere clients. import { revalidatePath } from 'next/cache' -import { z } from 'zod' import { prisma } from '@/lib/prisma' import { getSession } from '@/lib/auth' import { productAccessFilter } from '@/lib/product-access' - -const inputSchema = z.object({ - questionId: z.string().cuid(), - answer: z.string().min(1).max(4000), -}) +import { answerQuestionSchema } from '@/lib/schemas/question-answer' type ActionResult = { ok: true } | { ok: false; error: string } @@ -32,7 +27,7 @@ export async function answerQuestion( if (!session.userId) return { ok: false, error: 'Niet ingelogd' } if (session.isDemo) return { ok: false, error: 'Niet beschikbaar in demo-modus' } - const parsed = inputSchema.safeParse({ questionId, answer }) + const parsed = answerQuestionSchema.safeParse({ questionId, answer }) if (!parsed.success) { const first = parsed.error.issues[0]?.message ?? 'Ongeldige invoer' return { ok: false, error: first } diff --git a/components/notifications/answer-modal.tsx b/components/notifications/answer-modal.tsx index cbe574d..000868c 100644 --- a/components/notifications/answer-modal.tsx +++ b/components/notifications/answer-modal.tsx @@ -2,11 +2,11 @@ // ST-1105: Modal waar de gebruiker een Claude-vraag beantwoordt (M11). // -// Free-text Textarea (max 4000) 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 +// 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 tooltip. +// disabled met DemoTooltip. import { useState, useTransition } from 'react' import Link from 'next/link' @@ -16,18 +16,25 @@ import { Dialog, DialogContent, DialogDescription, - DialogHeader, DialogTitle, - DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +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' -const MAX_ANSWER_CHARS = 4000 - interface AnswerModalProps { question: NotificationQuestion | null isDemo: boolean @@ -38,26 +45,23 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) { const [answer, setAnswer] = useState('') const [pending, startTransition] = useTransition() - if (!question) return null + const closeGuard = useDirtyCloseGuard(answer.trim().length > 0, () => { + setAnswer('') + onClose() + }) - const charsLeft = MAX_ANSWER_CHARS - answer.length + 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 - if (isDemo) { - toast.error('Niet beschikbaar in demo-modus') - return - } startTransition(async () => { const res = await answerQuestion(question.id, text) if (!res.ok) { toast.error(res.error) return } - // Optimistisch verwijderen — SSE-event komt anders later met dezelfde - // remove en kost een extra render useNotificationsStore.getState().remove(question.id) toast.success('Antwoord verstuurd') setAnswer('') @@ -65,93 +69,107 @@ export function AnswerModal({ question, isDemo, onClose }: AnswerModalProps) { }) } + const handleKeyDown = useDialogSubmitShortcut(() => { + if (!submitDisabled) submit(answer) + }) + + if (!question) return null + return ( - !open && onClose()}> - - - Beantwoord Claude - - {question.story_code ?? 'story'} - {' — '} - {question.story_title} - - - + { if (!open) closeGuard.attemptClose() }}> + - - Open in Sprint - - -
- {question.question} -
- - {question.options && question.options.length > 0 ? ( -
-

Kies een van de opties:

-
- {question.options.map((opt) => ( - - ))} +
+
+ Beantwoord Claude + + {question.story_code ?? 'story'} + {' — '} + {question.story_title} +
- ) : ( -
-