espace-paie-odentas/components/ConfirmableForm.tsx
2025-10-12 17:05:46 +02:00

122 lines
No EOL
3.6 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
import type { FormHTMLAttributes, PropsWithChildren } from "react";
type Props = PropsWithChildren<
FormHTMLAttributes<HTMLFormElement> & {
confirmTitle: string;
confirmMessage: string;
confirmCta?: string;
cancelCta?: string;
disabled?: boolean;
submitLabel?: string; // when you want to render a default submit button
buttonClassName?: string;
// Nouveau: support pour les actions async (API calls)
action?: string | ((formData: FormData) => Promise<void>);
}
>;
export default function ConfirmableForm({
children,
confirmTitle,
confirmMessage,
confirmCta = "Confirmer",
cancelCta = "Annuler",
disabled,
submitLabel,
buttonClassName,
action,
...formProps
}: Props) {
const formRef = useRef<HTMLFormElement>(null);
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
function onSubmitIntercept(e: React.FormEvent<HTMLFormElement>) {
if (disabled || isSubmitting) return; // disabled => laisse le navigateur gérer
e.preventDefault();
setOpen(true);
}
async function doSubmit() {
if (!formRef.current || isSubmitting) return;
setIsSubmitting(true);
setOpen(false);
try {
if (typeof action === "function") {
// Action async personnalisée
const formData = new FormData(formRef.current);
await action(formData);
} else if (typeof action === "string") {
// URL d'action classique
formRef.current.action = action;
formRef.current.submit();
} else {
// Soumission classique
formRef.current.submit();
}
} catch (error) {
console.error("Erreur lors de la soumission:", error);
// L'erreur est gérée par le composant parent via l'action
} finally {
setIsSubmitting(false);
}
}
return (
<>
<form ref={formRef} onSubmit={onSubmitIntercept} {...formProps}>
{children}
{submitLabel && (
<button
type="submit"
disabled={disabled || isSubmitting}
className={
buttonClassName ||
"px-3 py-1 rounded bg-slate-900 text-white text-xs hover:bg-slate-700 disabled:opacity-50"
}
>
{isSubmitting ? "..." : submitLabel}
</button>
)}
</form>
{open && (
<div className="fixed inset-0 z-[100] grid place-items-center">
{/* backdrop */}
<div
className="absolute inset-0 bg-black/40"
onClick={() => setOpen(false)}
aria-hidden
/>
{/* modal */}
<div className="relative z-[101] w-[min(92vw,480px)] rounded-2xl border bg-white p-5 shadow-xl">
<h3 className="text-base font-semibold mb-1">{confirmTitle}</h3>
<p className="text-sm text-slate-600">{confirmMessage}</p>
<div className="mt-4 flex justify-end gap-2">
<button
type="button"
onClick={() => setOpen(false)}
disabled={isSubmitting}
className="px-3 py-1.5 rounded-lg border text-sm disabled:opacity-50"
>
{cancelCta}
</button>
<button
type="button"
onClick={doSubmit}
disabled={isSubmitting}
className="px-3 py-1.5 rounded-lg bg-rose-600 text-white text-sm hover:bg-rose-700 disabled:opacity-50"
>
{isSubmitting ? "..." : confirmCta}
</button>
</div>
</div>
</div>
)}
</>
);
}