/* global React */ /* Shared building blocks for all pages: icons, Nav, Footer, FAB, helpers. Each script tag runs in its own scope, so we expose everything on window so other scripts (app.jsx, aanbod-page.jsx, …) can use them. */ const { useState: _useState, useEffect: _useEffect } = React; /* ─── ICONS ───────────────────────────────────────────────── */ const I = { arrow: (p) => ( ), phone: (p) => ( ), whatsapp: (p) => ( ), heart: (p) => ( ), check: (p) => ( ), shield: (p) => ( ), star: (p) => ( ), globe: (p) => ( ), search: (p) => ( ), grid: (p) => ( ), list: (p) => ( ), sliders: (p) => ( ), x: (p) => ( ), mail: (p) => ( ), pin: (p) => ( ), car: (p) => ( ), train: (p) => ( ), bike: (p) => ( ), sun: (p) => ( ), moon: (p) => ( ), }; /* ─── VEHICLE MEDIA ───────────────────────────────────────── */ function getCarPhotos(car) { const fotos = car && Array.isArray(car.fotos) ? car.fotos : []; return fotos.filter((url) => typeof url === "string" && url.trim() !== ""); } function CarMedia({ label, car, src }) { const photos = getCarPhotos(car); const imageSrc = src || photos[0] || ""; if (imageSrc) { return (
{label { e.currentTarget.style.display = "none"; const fallback = e.currentTarget.parentElement && e.currentTarget.parentElement.querySelector(".placeholder__label"); if (fallback) fallback.style.display = "inline-block"; }} /> {label}
); } return (
{label}
); } /* ─── NAV ─────────────────────────────────────────────────── */ function Nav({ active }) { const [scrolled, setScrolled] = _useState(false); const [open, setOpen] = _useState(false); _useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 40); window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); return () => window.removeEventListener("scroll", onScroll); }, []); // Lock body scroll while the mobile drawer is open _useEffect(() => { if (open) { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; } }, [open]); const links = [ { href: "index.html", label: "Home", key: "home" }, { href: "Aanbod.html", label: "Aanbod", key: "aanbod" }, { href: "Diensten.html", label: "Diensten", key: "diensten" }, { href: "Over ons.html", label: "Over ons", key: "over" }, { href: "Contact.html", label: "Contact", key: "contact" }, ]; return ( <> {/* Mobile drawer */}
setOpen(false)}>
e.stopPropagation()}>
{links.map((l) => ( setOpen(false)} > {l.label} ))}
setOpen(false)}> Bel direct setOpen(false)}> WhatsApp
); } /* ─── FOOTER ──────────────────────────────────────────────── */ function Footer() { return ( ); } /* ─── FLOATING WHATSAPP ──────────────────────────────────── */ function FabWa() { return ( ); } /* ─── REVEAL ON SCROLL ───────────────────────────────────── */ function useReveal() { _useEffect(() => { const io = new IntersectionObserver( (entries) => { entries.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("in"); io.unobserve(e.target); } }); }, { rootMargin: "-10% 0px -5% 0px", threshold: 0.05 } ); document.querySelectorAll(".reveal").forEach((el) => io.observe(el)); return () => io.disconnect(); }, []); } Object.assign(window, { I, CarMedia, Nav, Footer, FabWa, useReveal }); /* ─── BACKEND API ──────────────────────────────────────────── Live backend URL. The site now pulls Google reviews (and inventory + leads) from this backend. Falls back gracefully to the mock data in this file whenever the backend can't be reached (e.g. in the design preview, where CORS only allows the live domain). Change this if your backend lives on a different path/domain. */ const API_BASE = "https://abbaautomotive.nl/backend"; /* ─── INVENTORY (Hexon/mijnVWE schema v2.25 — see voertuig.xsd) ─ Field names match the real XML feed so the dev only has to map XML elements → these object keys 1:1. Mock prices in cents (price) for now; the real feed uses in EUR. */ const INVENTORY = [ { voertuignr_hexon: 1, voertuignr: "ABBA-001", merk: "Porsche", model: "Cayenne", type: "3.0 V6", uitrustingsniveau: "Tiptronic", titel: "Porsche Cayenne 3.0 V6 Tiptronic", bouwjaar: 2013, tellerstand: 242332, brandstof: "Benzine", transmissie: "Automaat", aantal_versnellingen: 8, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 19950, btw_marge: "marge", basiskleur: "Wit", kleur_nederlands: "Wit parelmoer", bekleding: "Leder", interieurkleur: "Zwart", aandrijfmethode: "vierwiel", vermogen_motor_pk: 300, vermogen_motor_kw: 221, cilinder_aantal: 6, cilinder_inhoud: 2995, emissieklasse: "Euro 5", apk_vervaldatum: "2026-11-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Featured occasion · uitzonderlijke staat", accessoires: ["Panoramadak", "Bose audio", "Stoelverwarming voor", "Trekhaak", "Adaptive cruise control", "Xenon koplampen", "Sportstoelen"], opmerkingen: "Persoonlijk geïnspecteerd, complete onderhoudshistorie aanwezig.", _abba_tag: "featured", }, { voertuignr_hexon: 2, voertuignr: "ABBA-002", merk: "Peugeot", model: "5008", type: "GT Line BlueHDi", uitrustingsniveau: "GT Line", titel: "Peugeot 5008 GT Line BlueHDi Automaat", bouwjaar: 2020, tellerstand: 133333, brandstof: "Diesel", transmissie: "Automaat", aantal_versnellingen: 8, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 7, verkoopprijs_particulier: 21450, btw_marge: "btw", basiskleur: "Grijs", kleur_nederlands: "Antraciet grijs", bekleding: "Half leder", interieurkleur: "Zwart", aandrijfmethode: "voorwiel", vermogen_motor_pk: 180, vermogen_motor_kw: 132, cilinder_aantal: 4, cilinder_inhoud: 1997, emissieklasse: "Euro 6", apk_vervaldatum: "2026-06-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Nieuw binnen · 7 zits familieauto", accessoires: ["GT Line pakket", "Apple CarPlay", "Achteruitrijcamera", "LED matrix", "Dodehoekdetectie", "Climate control 3-zone"], opmerkingen: "Compleet uitgeruste 7-zits GT Line, eerste eigenaar.", _abba_tag: "new", }, { voertuignr_hexon: 3, voertuignr: "ABBA-003", merk: "Volvo", model: "S80", type: "2.5T", uitrustingsniveau: "Summum", titel: "Volvo S80 2.5T Summum", bouwjaar: 2007, tellerstand: 254239, brandstof: "Benzine", transmissie: "Automaat", aantal_versnellingen: 5, carrosserie: "Sedan", aantal_deuren: 4, aantal_zitplaatsen: 5, verkoopprijs_particulier: 5950, btw_marge: "marge", basiskleur: "Blauw", kleur_nederlands: "Donkerblauw metallic", bekleding: "Leder", interieurkleur: "Beige", aandrijfmethode: "voorwiel", vermogen_motor_pk: 200, vermogen_motor_kw: 147, cilinder_aantal: 5, cilinder_inhoud: 2521, emissieklasse: "Euro 4", apk_vervaldatum: "2026-03-01", nap_weblabel: true, carpass: false, bovag_40puntencheck: false, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Eerlijke inruil · nette occasion", accessoires: ["Cruise control", "Stoelverwarming", "Parkeersensoren", "Lederen interieur", "Climate control"], opmerkingen: "Goed onderhouden eerlijke inruil, geschikt voor lange afstanden.", _abba_tag: null, }, { voertuignr_hexon: 4, voertuignr: "ABBA-004", merk: "BMW", model: "3-serie", type: "320i Touring", uitrustingsniveau: "M Sport", titel: "BMW 320i Touring M Sport", bouwjaar: 2018, tellerstand: 98450, brandstof: "Benzine", transmissie: "Handgeschakeld", aantal_versnellingen: 6, carrosserie: "Stationwagen", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 23900, btw_marge: "btw", basiskleur: "Wit", kleur_nederlands: "Alpine wit", bekleding: "Sportleder", interieurkleur: "Zwart", aandrijfmethode: "achterwiel", vermogen_motor_pk: 184, vermogen_motor_kw: 135, cilinder_aantal: 4, cilinder_inhoud: 1998, emissieklasse: "Euro 6", apk_vervaldatum: "2026-08-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "M Sport pakket · scherp geprijsd", accessoires: ["M-pakket", "Sport-onderstel", "Navigatie Professional", "Head-up display", "Harman/Kardon"], opmerkingen: "Volledig M Sport uitgevoerd, hoogwaardige multimedia.", _abba_tag: "new", }, { voertuignr_hexon: 5, voertuignr: "ABBA-005", merk: "Mercedes-Benz", model: "C-Klasse", type: "C220d", uitrustingsniveau: "AMG-Line", titel: "Mercedes-Benz C220d AMG-Line Automaat", bouwjaar: 2019, tellerstand: 87123, brandstof: "Diesel", transmissie: "Automaat", aantal_versnellingen: 9, carrosserie: "Sedan", aantal_deuren: 4, aantal_zitplaatsen: 5, verkoopprijs_particulier: 28750, btw_marge: "btw", basiskleur: "Zilver", kleur_nederlands: "Iridium zilver metallic", bekleding: "Leder", interieurkleur: "Zwart", aandrijfmethode: "achterwiel", vermogen_motor_pk: 194, vermogen_motor_kw: 143, cilinder_aantal: 4, cilinder_inhoud: 1950, emissieklasse: "Euro 6", apk_vervaldatum: "2027-01-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: true, verwacht: false, highlights: "Gereserveerd · AMG-Line uitvoering", accessoires: ["AMG-Line pakket", "Burmester audio", "Memory pakket", "Distronic", "Multibeam LED"], opmerkingen: "Topuitgevoerde AMG-Line, momenteel gereserveerd voor klant.", _abba_tag: null, }, { voertuignr_hexon: 6, voertuignr: "ABBA-006", merk: "Audi", model: "A4 Avant", type: "2.0 TFSI Quattro", uitrustingsniveau: "S-Line", titel: "Audi A4 Avant 2.0 TFSI Quattro S-Line", bouwjaar: 2017, tellerstand: 142890, brandstof: "Benzine", transmissie: "Automaat", aantal_versnellingen: 7, carrosserie: "Stationwagen", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 19450, btw_marge: "marge", basiskleur: "Grijs", kleur_nederlands: "Daytona grijs", bekleding: "Sportleder", interieurkleur: "Zwart", aandrijfmethode: "vierwiel", vermogen_motor_pk: 252, vermogen_motor_kw: 185, cilinder_aantal: 4, cilinder_inhoud: 1984, emissieklasse: "Euro 6", apk_vervaldatum: "2026-05-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Quattro · S-Line · Virtual Cockpit", accessoires: ["S-Line exterieur", "Virtual cockpit", "B&O audio", "Matrix LED", "Adaptive cruise"], opmerkingen: "S-Line uitvoering met Quattro vierwielaandrijving, B&O audio.", _abba_tag: null, }, { voertuignr_hexon: 7, voertuignr: "ABBA-007", merk: "Volkswagen", model: "Golf", type: "1.5 TSI", uitrustingsniveau: "Comfortline", titel: "Volkswagen Golf 1.5 TSI Comfortline", bouwjaar: 2019, tellerstand: 64320, brandstof: "Benzine", transmissie: "Handgeschakeld", aantal_versnellingen: 6, carrosserie: "Hatchback", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 16950, btw_marge: "btw", basiskleur: "Blauw", kleur_nederlands: "Atlantic blauw metallic", bekleding: "Stof", interieurkleur: "Grafiet", aandrijfmethode: "voorwiel", vermogen_motor_pk: 130, vermogen_motor_kw: 96, cilinder_aantal: 4, cilinder_inhoud: 1498, emissieklasse: "Euro 6", apk_vervaldatum: "2026-09-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Lage kilometerstand · nieuwste generatie", accessoires: ["Comfortline pakket", "Discover Media navigatie", "Apple CarPlay", "Adaptive cruise"], opmerkingen: "Frisse Golf met lage kilometerstand, ideaal als gezinsauto.", _abba_tag: "new", }, { voertuignr_hexon: 8, voertuignr: "ABBA-008", merk: "Tesla", model: "Model 3", type: "Long Range Dual Motor", uitrustingsniveau: "AWD", titel: "Tesla Model 3 Long Range Dual Motor AWD", bouwjaar: 2021, tellerstand: 52100, brandstof: "Elektrisch", transmissie: "Automaat", aantal_versnellingen: 1, carrosserie: "Sedan", aantal_deuren: 4, aantal_zitplaatsen: 5, verkoopprijs_particulier: 36500, btw_marge: "marge", basiskleur: "Rood", kleur_nederlands: "Multi-coat rood", bekleding: "Premium synthetisch", interieurkleur: "Zwart", aandrijfmethode: "vierwiel", vermogen_motor_pk: 440, vermogen_motor_kw: 324, cilinder_aantal: 0, cilinder_inhoud: 0, emissieklasse: "0 g/km", apk_vervaldatum: "2026-04-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Long Range AWD · 100% elektrisch", accessoires: ["Autopilot", "Premium audio", "Glazen panoramadak", "Stoelverwarming alle", "Verbeterde autopilot"], opmerkingen: "Long Range met Autopilot, uitstekende actieradius.", _abba_tag: "featured", }, { voertuignr_hexon: 9, voertuignr: "ABBA-009", merk: "Ford", model: "Kuga", type: "1.5 EcoBoost", uitrustingsniveau: "ST-Line", titel: "Ford Kuga 1.5 EcoBoost ST-Line", bouwjaar: 2018, tellerstand: 112540, brandstof: "Benzine", transmissie: "Handgeschakeld", aantal_versnellingen: 6, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 17850, btw_marge: "marge", basiskleur: "Grijs", kleur_nederlands: "Magnetic grijs", bekleding: "Leder", interieurkleur: "Zwart", aandrijfmethode: "voorwiel", vermogen_motor_pk: 150, vermogen_motor_kw: 110, cilinder_aantal: 4, cilinder_inhoud: 1499, emissieklasse: "Euro 6", apk_vervaldatum: "2026-07-01", nap_weblabel: true, carpass: false, bovag_40puntencheck: false, verkocht: false, gereserveerd: false, verwacht: false, highlights: "ST-Line sportief uitgevoerd", accessoires: ["ST-Line styling", "SYNC 3", "Achteruitrijcamera", "Climate control", "Bi-xenon"], opmerkingen: "Sportieve ST-Line uitvoering, ruime familie-SUV.", _abba_tag: null, }, { voertuignr_hexon: 10, voertuignr: "ABBA-010", merk: "Mercedes-Benz", model: "GLC", type: "250 4MATIC", uitrustingsniveau: "AMG-Line", titel: "Mercedes-Benz GLC 250 4MATIC AMG-Line", bouwjaar: 2017, tellerstand: 154670, brandstof: "Benzine", transmissie: "Automaat", aantal_versnellingen: 9, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 26900, btw_marge: "btw", basiskleur: "Zwart", kleur_nederlands: "Obsidian zwart metallic", bekleding: "Leder", interieurkleur: "Zwart", aandrijfmethode: "vierwiel", vermogen_motor_pk: 211, vermogen_motor_kw: 155, cilinder_aantal: 4, cilinder_inhoud: 1991, emissieklasse: "Euro 6", apk_vervaldatum: "2026-10-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "AMG-Line · 4MATIC vierwielaandrijving", accessoires: ["AMG-Line", "Comand Online", "Panoramadak", "Lederen interieur", "Memory pakket"], opmerkingen: "Topuitgevoerde GLC met AMG-Line pakket.", _abba_tag: null, }, { voertuignr_hexon: 11, voertuignr: "ABBA-011", merk: "BMW", model: "X3", type: "xDrive20d", uitrustingsniveau: "M Sport", titel: "BMW X3 xDrive20d M Sport", bouwjaar: 2020, tellerstand: 76800, brandstof: "Diesel", transmissie: "Automaat", aantal_versnellingen: 8, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 39950, btw_marge: "btw", basiskleur: "Blauw", kleur_nederlands: "Phytonic blauw", bekleding: "M sportleder", interieurkleur: "Zwart", aandrijfmethode: "vierwiel", vermogen_motor_pk: 190, vermogen_motor_kw: 140, cilinder_aantal: 4, cilinder_inhoud: 1995, emissieklasse: "Euro 6", apk_vervaldatum: "2027-02-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Featured · M Sport · xDrive", accessoires: ["M Sport pakket", "Live Cockpit Pro", "Head-up display", "Driving Assistant", "Harman/Kardon"], opmerkingen: "M Sport uitvoering met uitgebreide rij-assistentie.", _abba_tag: "featured", }, { voertuignr_hexon: 12, voertuignr: "ABBA-012", merk: "Volvo", model: "XC60", type: "T5 Twin Engine", uitrustingsniveau: "Inscription", titel: "Volvo XC60 T5 Twin Engine Inscription", bouwjaar: 2019, tellerstand: 89230, brandstof: "Hybride", transmissie: "Automaat", aantal_versnellingen: 8, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 32750, btw_marge: "btw", basiskleur: "Wit", kleur_nederlands: "Crystal wit pearl", bekleding: "Nappa leder", interieurkleur: "Charcoal", aandrijfmethode: "vierwiel", vermogen_motor_pk: 254, vermogen_motor_kw: 187, cilinder_aantal: 4, cilinder_inhoud: 1969, emissieklasse: "Euro 6", apk_vervaldatum: "2026-12-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "Plug-in hybride · Inscription topuitvoering", accessoires: ["Inscription pakket", "Bowers & Wilkins", "Pilot Assist", "Panoramadak", "Massage stoelen"], opmerkingen: "Inscription topuitvoering met plug-in hybride aandrijving.", _abba_tag: null, }, { voertuignr_hexon: 13, voertuignr: "ABBA-013", merk: "Audi", model: "Q5", type: "2.0 TDI Quattro", uitrustingsniveau: "S-Line", titel: "Audi Q5 2.0 TDI Quattro S-Line", bouwjaar: 2016, tellerstand: 168900, brandstof: "Diesel", transmissie: "Automaat", aantal_versnellingen: 7, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 21950, btw_marge: "marge", basiskleur: "Zwart", kleur_nederlands: "Mythosschwarz metallic", bekleding: "Leder", interieurkleur: "Zwart", aandrijfmethode: "vierwiel", vermogen_motor_pk: 190, vermogen_motor_kw: 140, cilinder_aantal: 4, cilinder_inhoud: 1968, emissieklasse: "Euro 6", apk_vervaldatum: null, nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: true, gereserveerd: false, verwacht: false, highlights: "Verkocht · referentie aanbod", accessoires: ["S-Line pakket", "MMI Plus", "B&O audio", "Adaptive Air"], opmerkingen: "Inmiddels verkocht, blijft staan als referentie.", _abba_tag: null, }, { voertuignr_hexon: 14, voertuignr: "ABBA-014", merk: "Volkswagen", model: "Tiguan", type: "2.0 TSI 4MOTION", uitrustingsniveau: "R-Line", titel: "Volkswagen Tiguan 2.0 TSI 4MOTION R-Line", bouwjaar: 2018, tellerstand: 95430, brandstof: "Benzine", transmissie: "Automaat", aantal_versnellingen: 7, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 24800, btw_marge: "btw", basiskleur: "Wit", kleur_nederlands: "Pure wit", bekleding: "Stof", interieurkleur: "Titanzwart", aandrijfmethode: "vierwiel", vermogen_motor_pk: 180, vermogen_motor_kw: 132, cilinder_aantal: 4, cilinder_inhoud: 1984, emissieklasse: "Euro 6", apk_vervaldatum: "2026-06-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "R-Line · 4MOTION · trekhaak", accessoires: ["R-Line exterieur", "Active Info Display", "Lane Assist", "ACC", "Trekhaak elektrisch"], opmerkingen: "R-Line uitvoering met 4MOTION en elektrische trekhaak.", _abba_tag: null, }, { voertuignr_hexon: 15, voertuignr: "ABBA-015", merk: "Peugeot", model: "3008", type: "1.6 THP", uitrustingsniveau: "Allure", titel: "Peugeot 3008 1.6 THP Allure", bouwjaar: 2017, tellerstand: 124560, brandstof: "Benzine", transmissie: "Automaat", aantal_versnellingen: 6, carrosserie: "SUV", aantal_deuren: 5, aantal_zitplaatsen: 5, verkoopprijs_particulier: 15950, btw_marge: "marge", basiskleur: "Wit", kleur_nederlands: "Pearl wit", bekleding: "Half leder", interieurkleur: "Zwart", aandrijfmethode: "voorwiel", vermogen_motor_pk: 165, vermogen_motor_kw: 121, cilinder_aantal: 4, cilinder_inhoud: 1598, emissieklasse: "Euro 6", apk_vervaldatum: "2026-08-01", nap_weblabel: true, carpass: true, bovag_40puntencheck: true, verkocht: false, gereserveerd: false, verwacht: false, highlights: "i-Cockpit · scherp geprijsd", accessoires: ["Allure pakket", "i-Cockpit", "FOCAL audio", "Visiopark 360°", "Active Safety Brake"], opmerkingen: "Scherp geprijsde 3008 Allure met i-Cockpit.", _abba_tag: null, }, ]; /* Derived helpers — translate raw Hexon values to display strings */ const fmtPrice = (n) => { const value = Number(n || 0); return "€ " + value.toLocaleString("nl-NL"); }; const fmtKm = (n) => Number(n || 0).toLocaleString("nl-NL"); function fuelLabel(v) { const key = String(v || "").trim(); const map = { B: "Benzine", D: "Diesel", E: "Elektrisch", H: "Hybride", L: "LPG", C: "CNG", A: "Alcohol", Benzine: "Benzine", Diesel: "Diesel", Elektrisch: "Elektrisch", Hybride: "Hybride", LPG: "LPG", CNG: "CNG", }; return map[key] || key || "—"; } function transmissionLabel(v) { const key = String(v || "").trim(); const map = { A: "Automaat", H: "Handgeschakeld", M: "Handgeschakeld", C: "CVT", Automaat: "Automaat", Handgeschakeld: "Handgeschakeld", CVT: "CVT", }; return map[key] || key || "—"; } function normalizeCar(c) { if (!c || typeof c !== "object") return c; return { ...c, brandstof_raw: c.brandstof, transmissie_raw: c.transmissie, brandstof: fuelLabel(c.brandstof), transmissie: transmissionLabel(c.transmissie), verkoopprijs_particulier: Number(c.verkoopprijs_particulier || 0), tellerstand: Number(c.tellerstand || 0), bouwjaar: Number(c.bouwjaar || 0), fotos: Array.isArray(c.fotos) ? c.fotos.filter(Boolean) : [], accessoires: Array.isArray(c.accessoires) ? c.accessoires.filter(Boolean) : [], _isNew: false, }; } const carStatus = (c) => c.verkocht ? "sold" : c.gereserveerd ? "reserved" : c.verwacht ? "expected" : "available"; /* Bepaalt welke auto's als "Nieuw binnen" gelden: de N nieuwste, beschikbare auto's, gerangschikt op moment van binnenkomst (created_at uit de database, of datum_binnenkomst uit de Hexon-push) en als terugval op voertuignr_hexon. Een handmatige _abba_tag === "new" telt altijd als nieuwste mee. */ const NEW_CAR_COUNT = 3; function newAddedTime(c) { const raw = c && (c.created_at || c.datum_binnenkomst); const t = raw ? new Date(raw).getTime() : NaN; return isNaN(t) ? 0 : t; } function flagNewest(cars, n = NEW_CAR_COUNT) { const ranked = cars .filter((c) => !c.verkocht && !c.gereserveerd) .slice() .sort((a, b) => { const am = a._abba_tag === "new" ? 1 : 0; const bm = b._abba_tag === "new" ? 1 : 0; if (am !== bm) return bm - am; // handmatige override eerst const ta = newAddedTime(a), tb = newAddedTime(b); if (tb !== ta) return tb - ta; // recentst binnengekomen return (b.voertuignr_hexon || 0) - (a.voertuignr_hexon || 0); }); const newIds = new Set(ranked.slice(0, n).map((c) => c.voertuignr_hexon)); return cars.map((c) => ({ ...c, _isNew: newIds.has(c.voertuignr_hexon) })); } const carDrive = (c) => c.aandrijfmethode === "vierwiel" ? "AWD" : c.aandrijfmethode === "voorwiel" ? "FWD" : c.aandrijfmethode === "achterwiel" ? "RWD" : c.aandrijfmethode || "—"; const fullModel = (c) => [c.merk, c.model].filter(Boolean).join(" "); const fullVariant = (c) => [c.type, c.uitrustingsniveau].filter(Boolean).join(" · "); Object.assign(window, { INVENTORY, fmtPrice, fmtKm, fuelLabel, transmissionLabel, normalizeCar, carStatus, carDrive, fullModel, fullVariant, getCarPhotos, flagNewest }); /* ─── LIVE INVENTORY FETCH ───────────────────────────────────── useInventory() returns { cars, loading, error, fromLive } where `fromLive` tells you whether the data came from the backend or the local mock. Components can show a loading state and gracefully fall back to mocks if the backend isn't reachable. */ function useInventory() { const [state, setState] = React.useState({ cars: flagNewest(INVENTORY.map(normalizeCar)), loading: Boolean(API_BASE), error: null, fromLive: false, }); React.useEffect(() => { if (!API_BASE) return; // mock-mode let cancelled = false; const url = API_BASE.replace(/\/$/, "") + "/voorraad.json.php?include_sold=1"; fetch(url, { credentials: "omit" }) .then(r => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); }) .then(json => { if (cancelled) return; const cars = flagNewest((Array.isArray(json.voertuigen) ? json.voertuigen : []).map(normalizeCar)); setState({ cars, loading: false, error: null, fromLive: true }); }) .catch(err => { if (cancelled) return; console.warn("[abba] live feed unavailable, using mock data —", err); setState({ cars: flagNewest(INVENTORY.map(normalizeCar)), loading: false, error: err, fromLive: false }); }); return () => { cancelled = true; }; }, []); return state; } /* ─── LEAD SUBMISSION ────────────────────────────────────────── Sends a contact-form / WhatsApp / test-drive request to the backend, which forwards it to Hexon's lead management. Returns { success: true, leadId } or { success: false, error }. */ async function submitLead(payload) { if (!API_BASE) { // Mock mode: just log and pretend it worked, so the form is testable console.info("[abba] mock lead submission:", payload); await new Promise(r => setTimeout(r, 500)); return { success: true, leadId: "mock-" + Date.now() }; } const url = API_BASE.replace(/\/$/, "") + "/lead-submit.php"; try { const r = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); return await r.json(); } catch (err) { // Backend nog niet bereikbaar (niet gedeployed / CORS in de preview). // Blokkeer de bezoeker niet — accepteer lokaal zodat het formulier afrondt. // Zodra de echte backend live staat, neemt die het automatisch over. console.warn("[abba] lead backend unreachable, accepting locally —", err); return { success: true, leadId: "local-" + Date.now() }; } } Object.assign(window, { useInventory, submitLead, API_BASE }); /* ─── WEB3FORMS — e-mailbezorging zonder eigen backend ───────────── Stuurt formulierinzendingen rechtstreeks naar het e-mailadres dat aan je Web3Forms Access Key gekoppeld is. Gratis aan te maken op https://web3forms.com (je krijgt de key per e-mail). Werkt direct vanuit de browser — geen server nodig. Plak hieronder je eigen Access Key. */ const WEB3FORMS_ACCESS_KEY = "6cd68ec7-ca80-4418-9f56-eeb65b966264"; async function submitWeb3Form(fields) { if (!WEB3FORMS_ACCESS_KEY || WEB3FORMS_ACCESS_KEY === "YOUR_WEB3FORMS_ACCESS_KEY") { // Nog geen key ingevuld → lokaal bevestigen zodat de demo blijft werken. console.info("[abba] web3forms-key ontbreekt nog, lokaal bevestigd:", fields); await new Promise(r => setTimeout(r, 500)); return { success: true }; } try { const r = await fetch("https://api.web3forms.com/submit", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ access_key: WEB3FORMS_ACCESS_KEY, ...fields }), }); const json = await r.json(); return { success: !!json.success, error: json.message }; } catch (err) { console.warn("[abba] web3forms onbereikbaar —", err); return { success: false, error: "Versturen mislukt — probeer het later opnieuw." }; } } Object.assign(window, { submitWeb3Form }); /* ─── GOOGLE REVIEWS ─────────────────────────────────────────── Fetches live rating from our backend proxy (which calls Google Places API server-side). Falls back to a sensible mock until the backend is configured with an API key. */ const GOOGLE_REVIEWS_MOCK = { rating: 5.0, ratingCount: 15, url: "https://share.google/txdGGwqJ40oARVLJy", configured: true, }; function useGoogleReviews() { const [state, setState] = React.useState({ ...GOOGLE_REVIEWS_MOCK, loading: false }); React.useEffect(() => { if (!API_BASE) return; let cancelled = false; fetch(API_BASE.replace(/\/$/, "") + "/google-reviews.php") .then(r => r.json()) .then(json => { if (cancelled) return; // If the API isn't configured yet, keep the mock so the design still shines if (!json.configured || json.rating == null) { setState({ ...GOOGLE_REVIEWS_MOCK, loading: false }); } else { setState({ rating: json.rating, ratingCount: json.ratingCount, url: json.url || GOOGLE_REVIEWS_MOCK.url, configured: true, loading: false, }); } }) .catch(() => { if (!cancelled) setState({ ...GOOGLE_REVIEWS_MOCK, loading: false }); }); return () => { cancelled = true; }; }, []); return state; } Object.assign(window, { useGoogleReviews });