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>
);
}
|