/* ============================================================
Rent-a-Car — Mobility & Fleet service page
============================================================ */
/* date helper — YYYY-MM-DD with day offset */
function nigerToday(offsetDays = 0) {
const d = new Date(Date.now() + offsetDays * 86400000);
return d.toISOString().slice(0, 10);
}
const FLEET_CATS = [
{ k: "all", label: { en: "All", fr: "Tous" } },
{ k: "sedan", label: { en: "Sedans", fr: "Berlines" } },
{ k: "suv", label: { en: "SUVs & 4x4", fr: "SUV & 4x4" } },
{ k: "van", label: { en: "Vans", fr: "Vans" } },
{ k: "pickup", label: { en: "Pickups", fr: "Pickups" } },
];
/* Fleet now loads live from Supabase (see gg-supabase.js).
window.GG_DATA.fleet is filled before React mounts. */
const FLEET = (window.GG_DATA && window.GG_DATA.fleet) || [];
/* Local fleet images — exact match per model */
/* Fleet images: local fallback merged live with Supabase paths via Proxy */
const RAC_VEHICLE_PHOTOS_LOCAL = {
"Toyota Corolla": "assets/fleet/toyota-corolla.jpg",
"Hyundai Elantra": "assets/fleet/elantra.jpg",
"Toyota RAV4": "assets/fleet/toyota-rav4.webp",
"Toyota Land Cruiser": "assets/fleet/land-cruiser.jpg",
"Mercedes Sprinter": "assets/fleet/mercedes-sprinter.png",
"Toyota HiAce": "assets/fleet/toyota-hiace.webp",
"Toyota Hilux D/Cab": "assets/fleet/toyota-hilux.jpg",
"Ford Ranger": "assets/fleet/ford-ranger.jpg",
"Kia Sportage": "assets/fleet/kia-sportage.jpg",
"Toyota Prado": "assets/fleet/toyota-prado.jpg",
"Mitsubishi L200": "assets/fleet/mitsubishi-triton.jpg",
"Toyota Coaster": "assets/fleet/toyota-coaster.jpg",
};
const RAC_VEHICLE_PHOTOS = new Proxy(RAC_VEHICLE_PHOTOS_LOCAL, {
get(local, key) {
const live = (window.GG_DATA && window.GG_DATA.fleetImgs) || {};
return live[key] || local[key];
},
});
function RACImg({ name, cover }) {
const [err, setErr] = useState(false);
const src = RAC_VEHICLE_PHOTOS[name] || "assets/fleet/land-cruiser.jpg";
if (err) return
;
return (
setErr(true)} loading="lazy" />
);
}
/* ---------- Google-Maps-style pick-up autocomplete ---------- */
const NIGER_PLACES = [
{ main: "A\u00e9roport International Diori Hamani", sec: "Niamey · Airport pick-up", branch: true },
{ main: "Guero\u2019s Group \u2014 Niamey Centre", sec: "Bd de l\u2019Ind\u00e9pendance, Niamey", branch: true },
{ main: "Guero\u2019s Group \u2014 Rive Droite", sec: "Route de Torodi, Niamey", branch: true },
{ main: "Plateau", sec: "Niamey, Niger" },
{ main: "Yantala", sec: "Niamey, Niger" },
{ main: "Lazaret", sec: "Niamey, Niger" },
{ main: "Kouara Kano", sec: "Niamey, Niger" },
{ main: "Gamkall\u00e9", sec: "Niamey, Niger" },
{ main: "Maradi", sec: "R\u00e9gion de Maradi, Niger" },
{ main: "Zinder", sec: "R\u00e9gion de Zinder, Niger" },
{ main: "Agadez", sec: "R\u00e9gion d\u2019Agadez, Niger" },
{ main: "Tahoua", sec: "R\u00e9gion de Tahoua, Niger" },
{ main: "Dosso", sec: "R\u00e9gion de Dosso, Niger" },
{ main: "Arlit", sec: "R\u00e9gion d\u2019Agadez, Niger" },
{ main: "Tillab\u00e9ri", sec: "R\u00e9gion de Tillab\u00e9ri, Niger" },
{ main: "Diffa", sec: "R\u00e9gion de Diffa, Niger" },
];
function GoogleAttribution() {
return (
{L("powered by", "fourni par")}
G o o g l e
);
}
function PlacesAutocomplete({ defaultValue }) {
const [value, setValue] = useState(defaultValue || "");
const [open, setOpen] = useState(false);
const [active, setActive] = useState(0);
const boxRef = useRef(null);
useEffect(() => {
const h = (e) => { if (boxRef.current && !boxRef.current.contains(e.target)) setOpen(false); };
document.addEventListener("mousedown", h);
return () => document.removeEventListener("mousedown", h);
}, []);
const q = value.trim().toLowerCase();
const matches = (q === "" ? NIGER_PLACES : NIGER_PLACES.filter((p) => (p.main + " " + p.sec).toLowerCase().includes(q))).slice(0, 6);
const choose = (p) => { setValue(p.main); setOpen(false); };
const onKey = (e) => {
if (!open || !matches.length) return;
if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => Math.min(a + 1, matches.length - 1)); }
else if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); }
else if (e.key === "Enter") { e.preventDefault(); if (matches[active]) choose(matches[active]); }
else if (e.key === "Escape") setOpen(false);
};
return (
{ setValue(e.target.value); setOpen(true); setActive(0); }} onFocus={() => setOpen(true)} onKeyDown={onKey} placeholder={L("Search a city, branch or airport", "Ville, agence ou a\u00e9roport")} style={inpStyle} autoComplete="off" />
{open && (
{matches.length === 0 ? (
{L("No matching place", "Aucun lieu trouv\u00e9")}
) : matches.map((p, i) => (
setActive(i)} onClick={() => choose(p)} style={{ display: "flex", alignItems: "center", gap: 12, width: "100%", textAlign: "left", padding: "11px 16px", background: i === active ? "var(--paper-2)" : "transparent", borderBottom: "1px solid var(--line)" }}>
{p.main}
{p.sec}
))}
)}
);
}
/* ---------- Corporate / fleet-sales contact modal ---------- */
function FleetSalesModal({ onClose }) {
const [done, setDone] = useState(false);
const [sending, setSending] = useState(false);
const [error, setError] = useState("");
const [f, setF] = useState({ company: "", contact: "", phone: "", email: "", fleetSize: "", duration: "", message: "", _hp: "" });
const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
useEffect(() => {
document.body.classList.add("no-scroll");
return () => document.body.classList.remove("no-scroll");
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
if (f._hp) { setDone(true); return; }
if (!window.sb) { setError(L("Connection unavailable — please call us instead.", "Connexion indisponible \u2014 merci de nous appeler directement.")); return; }
setSending(true); setError("");
const { data: lead, error: err } = await window.sb.from("fleet_leads").insert({
company: f.company, contact_name: f.contact, phone: f.phone, email: f.email,
fleet_size: f.fleetSize, duration: f.duration, message: f.message || null,
}).select().single();
setSending(false);
if (err) { setError(L("Something went wrong — please try again or call us.", "Une erreur est survenue \u2014 merci de r\u00e9essayer ou de nous appeler.")); return; }
if (window.ggNotify) window.ggNotify("fleet_lead", lead);
setDone(true);
};
return ReactDOM.createPortal(
e.stopPropagation()} style={{ background: "var(--paper)", borderRadius: 6, width: "min(560px,100%)", maxHeight: "90vh", overflow: "auto", boxShadow: "0 40px 80px -30px rgba(0,0,0,.6)" }}>
{done ? (
{L("Request received!", "Demande envoy\u00e9e !")}
{L("Our fleet-sales team will get back to you within one business day to build a tailored proposal.", "Notre \u00e9quipe flotte vous recontacte sous un jour ouvr\u00e9 pour b\u00e2tir une proposition sur mesure.")}
{L("Done", "Termin\u00e9")}
) : (
)}
, document.body);
}
function RentACar() {
const [cat, setCat] = useState("all");
const [withDriver, setWithDriver] = useState(false);
const [booked, setBooked] = useState(null);
const [salesOpen, setSalesOpen] = useState(false);
const filtered = FLEET.filter((v) => cat === "all" || v.cat === cat);
return (
{/* HERO + BOOKING */}
} style={{ position: "absolute", inset: 0 }} />
Guero’s Group Rent-a-Car
{L("Rent-a-Car · A Guero\u2019s Group service", "Rent-a-Car · Un service du Guero\u2019s Group")}
{L(<>Wheels for the journey>, <>Des roues pour la route>)}.
{L("From a city sedan to a 14-seater van or a box truck — self-drive or chauffeured, by the day or the year. A clean, well-maintained fleet you can count on across Niger.", "De la berline urbaine au van 14 places ou au camion — en libre conduite ou avec chauffeur, \u00e0 la journ\u00e9e ou \u00e0 l\u2019ann\u00e9e. Une flotte propre et bien entretenue, partout au Niger.")}
{/* booking widget */}
setWithDriver(!withDriver)} style={{ display: "flex", alignItems: "center", gap: 10 }}>
{L("With a driver", "Avec chauffeur")}
{L("Free cancellation up to 24h before pick-up", "Annulation gratuite jusqu\u2019\u00e0 24h avant le d\u00e9part")}
{/* FLEET */}
{L("The fleet", "La flotte")}
{L("Pick your ride.", "Choisissez votre v\u00e9hicule.")}
{FLEET_CATS.map((c) => (
setCat(c.k)} style={{
fontFamily: "var(--fsemi)", fontWeight: 700, fontSize: 13.5, letterSpacing: "0.04em", textTransform: "uppercase",
padding: "9px 16px", borderRadius: 2, transition: "all .2s",
background: cat === c.k ? "var(--rent)" : "transparent", color: cat === c.k ? "var(--paper)" : "var(--ink)",
border: "1px solid " + (cat === c.k ? "var(--rent)" : "var(--line)"),
}}>{L(c.label.en, c.label.fr)}
))}
{filtered.length === 0 && window.GG_DATA && window.GG_DATA.loadError ? (
{L("We're having trouble loading the fleet right now.", "Nous avons un probl\u00e8me pour charger la flotte en ce moment.")}
window.location.reload()}>{L("Try again", "R\u00e9essayer")}
{L("Or call us at", "Ou appelez-nous au")} {CONTACT.mobile}
) : (
{filtered.map((v, i) => setBooked(v)} />)}
)}
{/* HOW IT WORKS */}
How it works
On the road in four steps.
{[
["Choose", "Pick a vehicle and your dates — self-drive or with a driver.", "search"],
["Confirm", "Send your details and a valid licence. We confirm in minutes.", "check"],
["Collect", "Pick up at a branch or have it delivered to your door or the airport.", "key"],
["Drive", "Hit the road with 24/7 roadside support behind you.", "car"],
].map(([h, d, ic], i) => (
))}
{[["clock", "24/7", "roadside support"], ["shield", "Fully", "insured fleet"], ["gauge", "Unlimited", "mileage included"], ["users", "35%", "of Niger's market"]].map(([ic, n, l]) => (
))}
{/* CORPORATE CTA */}
} style={{ aspectRatio: "5/4", borderRadius: 5 }} />
For business
Corporate & long-term leasing.
Put your team and goods on the road without the capital outlay. Monthly and annual plans, dedicated account management, and replacement vehicles included.
{["Fixed monthly billing — no surprise costs", "Servicing & maintenance handled by us", "Replacement vehicle within 24h", "Optional dedicated drivers"].map((t) => (
{t}
))}
setSalesOpen(true)}>Talk to fleet sales
{/* BOOKING MODAL */}
{booked &&
setBooked(null)} />}
{salesOpen && setSalesOpen(false)} />}
);
}
/* ---------- Fleet card ---------- */
function FleetCard({ v, withDriver, delay, onBook }) {
const [hover, setHover] = useState(false);
const price = v.price + (withDriver ? 25000 : 0);
return (
setHover(true)} onMouseLeave={() => setHover(false)}
style={{ background: "var(--paper-0)", border: "1px solid var(--line)", borderRadius: 5, overflow: "hidden", transition: "transform .2s, box-shadow .2s", transform: hover ? "translateY(-5px)" : "none", boxShadow: hover ? "0 26px 50px -28px rgba(0,0,0,.32)" : "none", display: "flex", flexDirection: "column", height: "100%" }}>
} style={{ aspectRatio: "16/10" }}>
{v.tag &&
{v.tag === "Popular" ? L("Popular", "Populaire") : v.tag} }
{v.name}
{[["users", v.seats + L(" seats", " places")], ["gear", v.trans === "Auto" ? L("Auto", "Auto") : L("Manual", "Manuelle")], ["bolt", v.fuel === "Petrol" ? L("Petrol", "Essence") : v.fuel === "Diesel" ? "Diesel" : L("Hybrid", "Hybride")]].map(([ic, tx]) => (
{tx}
))}
{price.toLocaleString()} {L("FCFA/day", "FCFA/jour")}
{withDriver &&
{L("incl. driver", "chauffeur inclus")}
}
{L("Reserve", "R\u00e9server")}
);
}
/* ---------- Booking modal ---------- */
function BookingModal({ v, withDriver, onClose }) {
const [done, setDone] = useState(false);
const [sending, setSending] = useState(false);
const [error, setError] = useState("");
const [f, setF] = useState({ name: "", phone: "", email: "", pickup: nigerToday(1), ret: nigerToday(4), notes: "", _hp: "" });
const set = (k, val) => setF((s) => ({ ...s, [k]: val }));
useEffect(() => {
document.body.classList.add("no-scroll");
return () => document.body.classList.remove("no-scroll");
}, []);
const price = v.price + (withDriver ? 25000 : 0);
const days = Math.max(1, Math.round((new Date(f.ret) - new Date(f.pickup)) / 86400000));
const handleSubmit = async (e) => {
e.preventDefault();
if (f._hp) { setDone(true); return; }
if (!window.sb) { setError(L("Connection unavailable — please call us instead.", "Connexion indisponible \u2014 merci de nous appeler directement.")); return; }
if (new Date(f.ret) < new Date(f.pickup)) { setError(L("Return date must be after pick-up date.", "La date de retour doit \u00eatre apr\u00e8s la date de d\u00e9part.")); return; }
setSending(true); setError("");
const { error: err } = await window.sb.from("bookings").insert({
vehicle_id: v.id, pickup_at: f.pickup, return_at: f.ret,
with_driver: !!withDriver, total_xof: price * days,
contact: { name: f.name, phone: f.phone, email: f.email || null },
notes: f.notes || null, payment_method: "card",
});
setSending(false);
if (err) {
if (err.code === "23P01") {
setError(L("These dates are no longer available for this vehicle — please pick different dates.", "Ces dates ne sont plus disponibles pour ce v\u00e9hicule \u2014 merci de choisir d'autres dates."));
} else {
setError(L("Something went wrong — please try again or call us.", "Une erreur est survenue \u2014 merci de r\u00e9essayer ou de nous appeler."));
}
return;
}
// Fire-and-forget notification — booking is already saved even if this fails.
// We don't read the row back: anon only has INSERT permission on bookings,
// not SELECT, so we build the notification from what we already have here.
if (window.ggNotify) window.ggNotify("booking", {
vehicle_name: v.name, pickup_at: f.pickup, return_at: f.ret,
total_xof: price * days, notes: f.notes || null,
contact: { name: f.name, phone: f.phone, email: f.email || null },
});
setDone(true);
};
return ReactDOM.createPortal(
e.stopPropagation()} style={{ background: "var(--paper)", borderRadius: 6, width: "min(560px,100%)", maxHeight: "90vh", overflow: "auto", boxShadow: "0 40px 80px -30px rgba(0,0,0,.6)" }}>
} style={{ aspectRatio: "16/8" }} />
{done ? (
{L("Reservation requested!", "R\u00e9servation envoy\u00e9e !")}
{L(<>We've got your request for the {v.name} . Our team will confirm availability and payment by WhatsApp shortly.>, <>Nous avons bien reçu votre demande pour le {v.name} . Notre équipe confirmera la disponibilité et le paiement par WhatsApp très vite.>)}
{L("Confirm via WhatsApp too →", "Confirmer aussi par WhatsApp →")}
{L("Done", "Termin\u00e9")}
) : (
set("_hp", e.target.value)} autoComplete="off" tabIndex={-1}
style={{ position: "absolute", width: 1, height: 1, opacity: 0, pointerEvents: "none" }} aria-hidden="true" />
{L("Reserve", "R\u00e9server")}
{v.name}
{price.toLocaleString()} FCFA
{L("per day", "par jour")}{withDriver ? L(" · incl. driver", " · chauffeur inclus") : ""}
set("notes", e.target.value)} style={{ ...fInp, resize: "vertical" }} placeholder={L("Delivery to airport, child seat, etc.", "Livraison a\u00e9roport, si\u00e8ge enfant, etc.")} />
{L(`${days} day(s) · Total ${(price*days).toLocaleString()} FCFA · Payment by card, confirmed via WhatsApp`, `${days} jour(s) · Total ${(price*days).toLocaleString()} FCFA · Paiement par carte, confirm\u00e9 par WhatsApp`)}
{error && {error}
}
{sending ? L("Sending…", "Envoi…") : <>{L("Request reservation", "Envoyer la r\u00e9servation")} >}
{L("Free cancellation up to 24h before pick-up", "Annulation gratuite jusqu\u2019\u00e0 24h avant le d\u00e9part")}
)}
, document.body);
}
function RField({ icon, label, children }) {
return (
);
}
Object.assign(window, { RentACar });