diff --git a/src/components/ui/ModalFeedbackLoop.tsx b/src/components/ui/ModalFeedbackLoop.tsx new file mode 100644 index 00000000..1e4623fc --- /dev/null +++ b/src/components/ui/ModalFeedbackLoop.tsx @@ -0,0 +1,143 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { Modal } from './Modal'; + +export type Rating = 1 | 2 | 3 | 4 | 5; + +export interface ModalFeedbackLoopProps { + isOpen: boolean; + title: string; + children: React.ReactNode; + /** Arbitrary context passed back via onFeedback; e.g. modal id, form values. */ + modalData: T; + onFeedback: (entry: { + rating: Rating; + comment?: string; + sourceData: T; + }) => Promise | void; + onClose: () => void; + size?: 'sm' | 'md' | 'lg' | 'xl' | 'full'; +} + +/** + * Modal Dialogs: Feedback Loop (#411). + * + * Wraps the existing `Modal` and queues a follow-up feedback dialog after the + * primary modal closes. The cycle = primary modal → feedback request → onClose, + * which is the "feedback loop" referenced in the issue title. + */ +export function ModalFeedbackLoop({ + isOpen, + title, + children, + modalData, + onFeedback, + onClose, + size = 'md', +}: ModalFeedbackLoopProps) { + const [awaitingFeedback, setAwaitingFeedback] = useState(false); + const [rating, setRating] = useState(null); + const [comment, setComment] = useState(''); + const [status, setStatus] = useState<'idle' | 'sending' | 'sent'>('idle'); + + useEffect(() => { + if (isOpen) { + setAwaitingFeedback(false); + setRating(null); + setComment(''); + setStatus('idle'); + } + }, [isOpen]); + + const handlePrimaryClose = useCallback(() => { + setAwaitingFeedback(true); + }, []); + + const handleSubmitFeedback = useCallback(async () => { + if (rating == null) return; + setStatus('sending'); + try { + await onFeedback({ + rating, + comment: comment.trim() || undefined, + sourceData: modalData, + }); + setStatus('sent'); + window.setTimeout(() => onClose(), 800); + } catch { + setStatus('idle'); + } + }, [rating, comment, modalData, onFeedback, onClose]); + + const handleSkip = useCallback(() => onClose(), [onClose]); + + return ( + <> + + {children} + + + {status === 'sent' ? ( +

+ Thanks for the feedback! +

+ ) : ( + <> +
+ How was that experience? +
+ {([1, 2, 3, 4, 5] as Rating[]).map((n) => ( + + ))} +
+
+