import { useState, useCallback, useRef, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; // Address search via our own API (no Supabase dependency) const API_BASE = import.meta.env.VITE_API_BASE || ''; import { Search, MapPin, CheckCircle2, XCircle, Loader2, ArrowLeft, Zap, Mail, Phone, Send, } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import { z } from "zod"; const phoneClean = (v: string) => v.replace(/[\s\-().]/g, ""); const contactSchema = z.union([ z.string().trim().email().max(254), z.string().trim().transform(phoneClean).pipe( z.string().regex(/^\+?1?\d{10,11}$/, "Numéro invalide") ), ]); const CONTACT_COOLDOWN_MS = 5000; interface AddressResult { id: number; address_full: string; numero: string; rue: string; ville: string; code_postal: string; longitude: number; latitude: number; fiber_available: boolean; zone_tarifaire: number; max_speed: number; similarity_score: number; } type Step = "search" | "result"; export function AvailabilityDialog({ open, onOpenChange, }: { open: boolean; onOpenChange: (open: boolean) => void; }) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [selected, setSelected] = useState(null); const [step, setStep] = useState("search"); const [checking, setChecking] = useState(false); const [contactValue, setContactValue] = useState(""); const [contactSent, setContactSent] = useState(false); const [sendingContact, setSendingContact] = useState(false); const [contactError, setContactError] = useState(""); const lastSubmitRef = useRef(0); const debounceRef = useRef>(); // Reset on close useEffect(() => { if (!open) { setTimeout(() => { setQuery(""); setResults([]); setSelected(null); setStep("search"); setContactValue(""); setContactSent(false); }, 300); } }, [open]); // Cleanup debounce on unmount useEffect(() => { return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, []); const searchAddresses = useCallback(async (term: string) => { if (term.length < 3) { setResults([]); return; } setSearching(true); try { const res = await fetch(API_BASE + '/rpc/search_addresses', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ search_term: term, result_limit: 8 }), }); if (res.ok) { const data = await res.json(); setResults(data as AddressResult[]); } } finally { setSearching(false); } }, []); const handleQueryChange = (value: string) => { setQuery(value); if (debounceRef.current) clearTimeout(debounceRef.current); debounceRef.current = setTimeout(() => searchAddresses(value), 350); }; const handleValidate = async (address: AddressResult) => { setSelected(address); setChecking(true); setStep("result"); // Log the search (fire and forget) try { console.log('[Search]', query, '→', address.address_full, address.fiber_available); } catch (e) { console.error("Log search error:", e); } // Small delay for UX await new Promise((r) => setTimeout(r, 800)); setChecking(false); }; const handleContactSubmit = async () => { if (!contactValue.trim() || !selected) return; // Rate limit const now = Date.now(); if (now - lastSubmitRef.current < CONTACT_COOLDOWN_MS) { setContactError("Veuillez patienter quelques secondes."); return; } // Validate contact const result = contactSchema.safeParse(contactValue.trim()); if (!result.success) { setContactError("Veuillez entrer un courriel ou numéro de mobile valide."); return; } setContactError(""); setSendingContact(true); lastSubmitRef.current = now; const validContact = result.data; const isEmail = validContact.includes("@"); // Send lead notification via Mailjet try { await fetch(API_BASE + '/rpc/lead', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ contact: validContact, address: selected.address_full, fiber_available: selected.fiber_available, max_speed: selected.max_speed || 0, }), }); } catch (e) { console.error('Lead send error:', e); } setSendingContact(false); setContactSent(true); }; const speedLabel = (speed: number) => { if (!speed || speed === 0) return "1.5 Gbit/s"; if (speed >= 1000) return `${(speed / 1000).toFixed(1)} Gbit/s`; return `${speed} Mbit/s`; }; return ( Vérifier la disponibilité Entrez votre adresse pour vérifier si la fibre optique est disponible {step === "search" && ( {/* Search input */}
handleQueryChange(e.target.value)} className="pl-10" autoFocus /> {searching && ( )}
{/* Results list */}
{results.length === 0 && query.length >= 3 && !searching && (

Aucune adresse trouvée

)} {results.map((addr) => ( ))}
)} {step === "result" && selected && ( {checking ? (

Vérification en cours…

) : selected.fiber_available ? ( /* ✅ AVAILABLE */

Félicitations !

Le service est disponible à votre adresse jusqu'à{" "} {speedLabel(selected.max_speed)}

Votre forfait

À partir de 39,95$/mois

{selected.address_full}

{!contactSent ? (

Inscrivez votre courriel ou numéro de mobile pour démarrer votre abonnement

{ setContactValue(e.target.value); setContactError(""); }} className="flex-1" maxLength={254} />
{contactError &&

{contactError}

}
) : (
Merci ! Nous vous contacterons très bientôt.
)}
) : ( /* ❌ NOT AVAILABLE */

Adresse non confirmée

Nous n'arrivons pas à confirmer la disponibilité pour votre adresse.

{selected.address_full}

ou inscrivez votre courriel et nous vous contacterons
{!contactSent ? (
{ setContactValue(e.target.value); setContactError(""); }} className="flex-1" maxLength={254} />
{contactError &&

{contactError}

}
) : (
Merci ! Nous vous contacterons très bientôt.
)}
)}
)}
); }