All files / src/components/projects/modals AdditionalInfoModal.tsx

100% Statements 16/16
100% Branches 8/8
100% Functions 6/6
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122                            33x     33x   33x 13x           33x       33x 6x 3x   3x       33x   33x 3x 3x         33x                                                                                         14x                                               1x          
import { useState, useMemo, type FormEvent } from "react";
import { X } from "lucide-react";
import { Button } from "../../ui/button";
import { ConfirmDialog } from "../../ui/confirm-dialog";
import { useUnsavedChanges, useDialogAccessibility } from "../../../hooks";
import type { Project } from "../../../lib/api";
 
interface AdditionalInfoModalProps {
	project: Project;
	onSave: (data: Partial<Project>) => void;
	onClose: () => void;
}
 
export function AdditionalInfoModal({ project, onSave, onClose }: AdditionalInfoModalProps) {
	const [clientTestimonial, setClientTestimonial] = useState(
		project.clientTestimonial || "",
	);
	const [showDiscardConfirm, setShowDiscardConfirm] = useState(false);
 
	const initialValues = useMemo(
		() => ({
			clientTestimonial: project.clientTestimonial || "",
		}),
		[project],
	);
 
	const isDirty = useUnsavedChanges(initialValues, {
		clientTestimonial,
	});
 
	const handleClose = () => {
		if (isDirty) {
			setShowDiscardConfirm(true);
		} else {
			onClose();
		}
	};
 
	const { dialogRef, handleFocusTrap } = useDialogAccessibility(handleClose);
 
	const handleSubmit = (e: FormEvent) => {
		e.preventDefault();
		onSave({
			clientTestimonial: clientTestimonial || null,
		});
	};
 
	return (
		<div
			role="dialog"
			className="fixed inset-0 flex items-center justify-center z-50"
			onKeyDown={handleFocusTrap}
		>
			<button
				type="button"
				aria-label="Close modal"
				className="fixed inset-0 bg-black/50 cursor-default"
				onClick={handleClose}
				tabIndex={-1}
			/>
			<div
				ref={dialogRef}
				role="dialog"
				aria-modal="true"
				aria-labelledby="additional-info-modal-title"
				tabIndex={-1}
				className="relative bg-background-elevated rounded-lg shadow-xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto focus:outline-none"
			>
				<div className="flex items-center justify-between p-4 border-b">
					<h3 id="additional-info-modal-title" className="font-semibold">
						Edit Additional Info
					</h3>
					<button
						type="button"
						onClick={handleClose}
						aria-label="Close"
						className="text-foreground-subtle hover:text-foreground-muted"
					>
						<X className="h-5 w-5" />
					</button>
				</div>
				<form onSubmit={handleSubmit} className="p-4 space-y-4">
					<div className="space-y-2">
						<label
							htmlFor="ai-testimonial"
							className="text-sm font-medium text-foreground-default"
						>
							Client Testimonial
						</label>
						<textarea
							id="ai-testimonial"
							value={clientTestimonial}
							onChange={(e) => setClientTestimonial(e.target.value)}
							placeholder="Share what the client said about this project..."
							className="flex min-h-[80px] w-full rounded-md border border-default bg-background-elevated px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary-500/40 focus:border-primary-500"
							maxLength={5000}
						/>
						<p className="text-xs text-foreground-subtle text-right">
							{clientTestimonial.length}/5000
						</p>
					</div>
					<div className="flex justify-end gap-2 pt-4">
						<Button type="button" variant="outline" onClick={handleClose}>
							Cancel
						</Button>
						<Button type="submit">Save</Button>
					</div>
				</form>
			</div>
			<ConfirmDialog
				open={showDiscardConfirm}
				title="Discard unsaved changes?"
				description="You have unsaved changes that will be lost if you close this form."
				confirmLabel="Discard"
				variant="destructive"
				onConfirm={onClose}
				onCancel={() => setShowDiscardConfirm(false)}
			/>
		</div>
	);
}