{deal.title}
{deal.subtitle && ({deal.subtitle}
)}/* global React, Icon, chf */
// ACN-Cars · Page: Home (Hero + featured + deals + marquee)
// ACN v2.4 (Claude, Okt 2026) — Profi-Überarbeitung: Aktion im Hero, Mindestalter, Sortierung,
// echte Marken-Leiste, Anlässe + Gutschein, ehrliche Vorteile-Texte, Header-CTA, Handy-Leiste.
// v2.2.41 — Slot-Helper für Aktion-Karten. Berechnet das nächste Vorkommen
// eines konkreten Wochentag+Stunden-Slots ab `now`. Wenn heute der Slot-Tag
// ist UND die Slot-Uhrzeit bereits verstrichen ist, springt er auf nächste
// Woche — exakt das Verhalten, das v2.2.41 für „Aktion-Slot-Strict" will.
function acnNextDateForDow(now, targetDow, hour) {
const d = new Date(now.getTime());
d.setHours(hour || 9, 0, 0, 0);
let diff = (targetDow - d.getDay() + 7) % 7;
if (diff === 0 && d.getTime() < now.getTime()) diff = 7;
d.setDate(d.getDate() + diff);
return d;
}
function acnFmtSlot(d) {
if (!d || isNaN(d.getTime())) return "";
const dows = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
const months = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"];
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
return `${dows[d.getDay()]} ${d.getDate()}. ${months[d.getMonth()]} · ${hh}:${mm}`;
}
// ACN v2.3 (Claude, Okt 2026) — Fahrzeug-Gruppen für Hero-Auswahl + Flotten-Filter
function acnVehGroup(v) {
const s = ((v.brand || "") + " " + (v.model || "")).toLowerCase();
if (/crafter|transporter|sprinter|lieferwagen|ducato|transit|jumper|boxer/.test(s)) return "transporter";
const c = String(v.category || "").toLowerCase();
if (c === "sport" || c === "luxus" || c === "cabrio") return "sport";
return "alltag";
}
const ACN_GROUP_META = {
sport: { label: "Sportwagen & AMG", short: "Sportwagen & AMG" },
alltag: { label: "Alltag & Familie", short: "Alltag & Familie" },
transporter: { label: "Transporter", short: "Transporter" },
};
function acnBrandLabel(v) {
const b = String(v.brand || "").trim(), m = String(v.model || "");
if (/^audi$/i.test(b) && /^rs/i.test(m)) return "AUDI RS";
if (/mercedes/i.test(b) && /amg/i.test(m)) return "MERCEDES-AMG";
if (/^vw$|volkswagen/i.test(b)) return "VOLKSWAGEN";
return b.toUpperCase();
}
function acnHasPromo(v) { return Number(v.oldPrice) > Number(v.pricePerDay) && Number(v.pricePerDay) > 0; }
const ACN_GROUP_ORDER = { sport: 0, alltag: 1, transporter: 2 };
function acnSortFleet(list) {
return list.slice().sort((a, b) => {
const pa = acnHasPromo(a) ? 0 : 1, pb = acnHasPromo(b) ? 0 : 1;
if (pa !== pb) return pa - pb;
const ga = ACN_GROUP_ORDER[acnVehGroup(a)], gb = ACN_GROUP_ORDER[acnVehGroup(b)];
if (ga !== gb) return ga - gb;
return (Number(b.pricePerDay) || 0) - (Number(a.pricePerDay) || 0);
});
}
const ACN_OCCASIONS = [
{ key: "sport", icon: "💍", title: "Hochzeit & Events", text: "Der perfekte Auftritt für Ihren grossen Tag – G63, AMG GT oder RS6." },
{ key: "sport", icon: "🎁", title: "Geburtstag & Erlebnis", text: "Einmal selbst Sportwagen fahren – als Highlight oder Überraschung." },
{ key: "alltag", icon: "👨👩👧", title: "Familie & Ferien", text: "Platz für alle: 7-Plätzer oder komfortabler SUV für Ausflug und Reise." },
{ key: "transporter", icon: "📦", title: "Umzug & Transport", text: "VW Crafter für Umzug, Möbel oder Lieferungen – fahrbar mit Ausweis Kat. B." },
];
function acnShortModel(v) {
const words = String(v.model || "").replace(/\b4Matic\+?|quattro|TDI|Executive|xDrive\S*/gi, "").trim().split(/\s+/);
return words.slice(0, 2).join(" ");
}
function HomePage({ vehicles, deals, locations, favs, onFav, onCompare, comparing, onBook, onOpen, onDealBook, goPage, contact, boot }) {
// v2.2.23 — Zwei separate Featured-Toggles statt einem:
// featuredHero → steuert nur den Hero-Slider oben
// featuredCards → steuert Aktion-Karten + Live-Kalkulator
// Damit kann der User z.B. 3 Premium-Autos im Hero zeigen und 6 andere
// Fahrzeuge in den Aktion-Karten / im Kalkulator. Bleibt eines der beiden
// Felder komplett leer, fällt es auf die alte v2.2.22-Logik zurück (alle
// Vehicles), damit nichts leer wirkt. Die Flotte-Seite zeigt unverändert
// immer alle Fahrzeuge — unabhängig von beiden Toggles.
// ACN v2.5 — Zurück-Knopf: Klick auf Auto/Buchen legt einen Verlaufs-Eintrag an,
// damit «Zurück» im Browser wieder zur Flotte (gleiche Scroll-Position) führt
// statt die Seite zu verlassen.
const acnPush = () => {
try {
window.__acnScrollY = window.scrollY;
if (!(window.history.state && window.history.state.acnView === "sub")) {
window.history.pushState({ acnView: "sub" }, "");
}
} catch (e) {}
};
const _acnOnOpen = onOpen, _acnOnBook = onBook, _acnOnDealBook = onDealBook, _acnGoPage = goPage;
if (_acnOnOpen) onOpen = (...a) => { acnPush(); return _acnOnOpen(...a); };
if (_acnOnBook) onBook = (...a) => { acnPush(); return _acnOnBook(...a); };
if (_acnOnDealBook) onDealBook = (...a) => { acnPush(); return _acnOnDealBook(...a); };
if (_acnGoPage) goPage = (p) => { if (p !== "home") acnPush(); return _acnGoPage(p); };
window.__acnGoHome = () => _acnGoPage && _acnGoPage("home");
React.useEffect(() => {
if (!window.__acnPopBound) {
window.__acnPopBound = true;
window.addEventListener("popstate", (ev) => {
if (ev.state && ev.state.acnView === "sub") return;
window.__acnRestore = true;
if (window.__acnGoHome) window.__acnGoHome();
});
}
try {
if (window.__acnRestore) {
window.__acnRestore = false;
const y = window.__acnScrollY || 0;
setTimeout(() => window.scrollTo(0, y), 60);
} else if (window.history.state && window.history.state.acnView === "sub") {
// per Knopf zurück zur Übersicht → überzähligen Verlaufs-Eintrag entfernen
window.history.back();
}
} catch (e) {}
}, []);
const heroFeatured = vehicles.filter(v => v.featuredHero);
const cardFeatured = vehicles.filter(v => v.featuredCards);
const heroVehicles = heroFeatured.length > 0 ? heroFeatured : vehicles;
const cardVehicles = cardFeatured.length > 0 ? cardFeatured : vehicles;
// homeVehicles bleibt als Convenience-Alias für Stellen, die „auf Startseite
// sichtbar" allgemein meinen (wir nehmen die Karten-Liste als kanonisch,
// weil das die grössere Auswahl-Sektion ist).
const homeVehicles = cardVehicles;
// Hero-Slider: Top-Modelle, automatischer Wechsel alle 5 s
const heroSlides = heroVehicles.slice(0, 5);
const [heroIdx, setHeroIdx] = React.useState(0);
React.useEffect(() => {
if (heroSlides.length < 2) return;
const t = setInterval(() => setHeroIdx(i => (i + 1) % heroSlides.length), 5000);
return () => clearInterval(t);
}, [heroSlides.length]);
const heroVehicle = heroSlides[heroIdx] || heroVehicles[0] || vehicles[0];
// ACN v2.3 — Flotten-Filter (wird von Hero-Kacheln und Filter-Tabs gesetzt)
const [fleetFilter, setFleetFilter] = React.useState("all");
const pickGroup = (key) => {
setFleetFilter(key);
setTimeout(() => {
const el = document.getElementById("alle-fahrzeuge");
if (el && el.scrollIntoView) el.scrollIntoView({ behavior: "smooth", block: "start" });
}, 30);
};
const heroGroups = ["sport", "alltag", "transporter"].map((key) => {
const list = vehicles.filter((v) => acnVehGroup(v) === key);
if (!list.length) return null;
const cheapest = list.reduce((a, b) => ((Number(b.pricePerDay) || 1e9) < (Number(a.pricePerDay) || 1e9) ? b : a), list[0]);
let sub = list.map(acnShortModel).join(" · ");
if (key === "transporter") sub = list.map(acnShortModel).join(" · ") + " · für Umzug & Transport";
if (key === "alltag") sub = list.map(v => (Number(v.seats) >= 7 ? acnShortModel(v) + " 7-Plätzer" : acnShortModel(v))).join(" · ");
return { key, label: ACN_GROUP_META[key].label, sub, image: cheapest.image, minDay: Number(cheapest.pricePerDay) || 0, count: list.length };
}).filter(Boolean);
const heroFeatures = [
"Vollkasko inklusive",
"Ab 21 J. · 3 J. Ausweis",
"Gratis-Storno bis 24 h",
];
// ACN v2.4 — Aktion (Streichpreis) prominent im Hero
const promoVehicle = acnSortFleet(vehicles.filter(acnHasPromo))[0] || null;
// ACN v2.4 — Telefon/WhatsApp für Header-CTA + Handy-Leiste
const telHref = "tel:+41" + String((contact && contact.phone) || "052 670 15 00").replace(/\D/g, "").replace(/^0/, "");
const waHref = ((contact && contact.whatsappLink) ? contact.whatsappLink.split("?")[0] : "https://wa.me/41789480304");
const openBooking = () => goPage("booking");
React.useEffect(() => {
// Header: Telefon + «Jetzt anfragen» rechts im Kopfbereich (nur auf dieser Seite)
const host = document.querySelector("#site-header .e-con-inner") || document.querySelector("#site-header-inner");
if (!host || host.querySelector(".acn-mh-hdr")) return;
const wrap = document.createElement("div");
wrap.className = "acn-mh-hdr";
const tel = document.createElement("a");
tel.className = "acn-mh-hdr-tel"; tel.href = telHref;
tel.textContent = (contact && contact.phone) || "052 670 15 00";
const btn = document.createElement("button");
btn.type = "button"; btn.className = "acn-mh-hdr-btn"; btn.textContent = "Jetzt anfragen";
btn.addEventListener("click", () => {
const b = document.querySelector(".acn-hx-cta-row .btn-primary-outline");
if (b) { b.click(); window.scrollTo({ top: 0, behavior: "smooth" }); }
else { const el = document.getElementById("alle-fahrzeuge"); if (el) el.scrollIntoView({ behavior: "smooth" }); else window.scrollTo({ top: 0, behavior: "smooth" }); }
});
wrap.appendChild(tel); wrap.appendChild(btn); host.appendChild(wrap);
// v2.5.4 — Fussbereich: «Lieferung & Abholung» → «Mietbedingungen» (springt zur FAQ), «Versicherung» + «AGB» per CSS ausgeblendet
const fixFooter = () => {
const lis = document.querySelectorAll(".acn-app-root .footer-grid > div:nth-child(3) li");
if (lis.length < 3 || lis[2].dataset.acnMb) return;
lis[2].dataset.acnMb = "1";
const a = lis[2].querySelector("a");
if (a) { a.textContent = "Mietbedingungen"; a.setAttribute("href", "#mietbedingungen"); }
lis[2].addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
const t = Array.from(document.querySelectorAll(".acn-seo-section h2")).find(h => /Häufige Fragen/.test(h.textContent));
if (t) t.scrollIntoView({ behavior: "smooth", block: "start" });
}, true);
};
fixFooter(); setTimeout(fixFooter, 800);
document.body.classList.add("acn-miete-v24");
// Bilder ohne Alt-Text (SEO/Barrierefreiheit)
document.querySelectorAll("img:not([alt]), img[alt='']").forEach((im) => {
im.setAttribute("alt", /logo/i.test(im.className + im.src) ? "ACN-Cars GmbH Logo" : "ACN-Cars Mietfahrzeug");
});
}, []);
// v2.2.14 — Bewertung aus Backend (boot.rating / boot.ratingCount), mit
// sinnvollen Defaults, falls noch nicht gesetzt.
const ratingValue = (boot && boot.rating != null && String(boot.rating).trim() !== "")
? String(boot.rating).trim()
: "4.9";
const ratingCount = (boot && boot.ratingCount != null && String(boot.ratingCount).trim() !== "")
? String(boot.ratingCount).trim()
: "327";
const showLocations = boot ? !!boot.showLocations : (locations && locations.length > 1);
// v2.2.20 — „Warum ACN-Cars"-Karten aus dem Backend (Title + Text editierbar,
// Icons bleiben hardcoded). Fallback auf Defaults, falls boot.whyCards fehlt.
const whyCardsBoot = (boot && Array.isArray(boot.whyCards)) ? boot.whyCards : [];
const whyCardFallbacks = [
{
title: "Geprüfte Premium-Fahrzeuge",
text: "Jedes Auto wird vor jeder Übergabe von unserem Team komplett aufbereitet — Innenraum, Lack, Technik. Sie steigen ein und fahren los.",
},
{
title: "24/7 Concierge",
text: "Persönlicher Ansprechpartner per WhatsApp, SMS oder Anruf — rund um die Uhr.",
},
{
title: "Lieferung zum Wunschort",
text: "Wir bringen Ihr Fahrzeug — bis vor die Haustür, zum Hotel oder direkt zum Flughafen. Schweizweit.",
},
{
title: "Transparente Preise",
text: "Keine versteckten Gebühren, keine bösen Überraschungen. Inkl. MwSt., inkl. km, inkl. Versicherung — was Sie sehen, ist was Sie zahlen.",
},
];
const whyCard = (i) => {
const fallback = whyCardFallbacks[i] || { title: "", text: "" };
const fromBoot = whyCardsBoot[i];
if (!fromBoot) return { ...fallback, enabled: true };
return {
title: (fromBoot.title && String(fromBoot.title).trim() !== "") ? fromBoot.title : fallback.title,
text: (fromBoot.text && String(fromBoot.text).trim() !== "") ? fromBoot.text : fallback.text,
enabled: fromBoot.enabled !== false,
};
};
// v2.2.13 — dealVehicles erkennt jetzt auch Tier-Aktionen (3h/6h/1w),
// damit ein Fahrzeug mit z.B. nur einer 6h-Aktion korrekt in der
// Aktionen-Sektion erscheint.
const hasAnyTierDeal = (v) => {
const tp = v.tierPrices || {};
const top = v.tierOldPrices || {};
return ["3", "6", "24", "168"].some(h => {
const price = h === "24" ? (tp[h] || v.pricePerDay) : tp[h];
const oldP = top[h];
return price && oldP && Number(oldP) > 0 && Number(oldP) > Number(price);
});
};
// v2.2.42 — Map vehicle.id → matchender Deal-CPT, plus dealType-Info.
// Wird genutzt, um Duplikate zwischen Aktions-Sektion (oben) und
// „Alle Fahrzeuge"-Grid (unten) zu vermeiden:
// - Discount-Aktion → Auto NUR oben sichtbar (unten ausgeblendet)
// - Package-Aktion → Auto OBEN als Aktion-Karte UND UNTEN mit Slot-Hinweis
// (damit es Mo-Do trotzdem buchbar bleibt)
// Bei Mehrfach-Treffern gewinnt 'package' über 'discount', weil package
// mehr Info trägt (Slot + Pauschale).
const vehicleDealMap = {};
(deals || []).forEach(deal => {
if (!Array.isArray(deal.vehicles)) return;
deal.vehicles.forEach(vid => {
const existing = vehicleDealMap[vid];
if (!existing) {
vehicleDealMap[vid] = deal;
} else if (deal.dealType === 'package' && existing.dealType !== 'package') {
vehicleDealMap[vid] = deal;
}
});
});
// v2.2.22 — Aktion-Karten zeigen nur Fahrzeuge, die auch auf der Startseite
// sichtbar sind (homeVehicles). So bleibt die Steuerung über `featured`
// konsistent — wer den Toggle abschaltet, taucht weder im Slider noch in
// den Aktion-Karten oder im Live-Kalkulator auf.
// v2.2.42 — Filter eng gezogen: Auto erscheint NUR in der Aktion-Sektion,
// wenn es tatsächlich in einem aktiven Deal-CPT ist. Vorher landete z.B.
// ein Crafter mit reduzierter Tier-Pauschale (ohne Deal-CPT) hier,
// ohne Aktion-Badge — Naim's Wunsch: nur „echte" Aktionen anzeigen.
// Tier-Rabatte allein sind keine Aktion mehr; sie bleiben unten im
// „Alle Fahrzeuge"-Grid mit der ganz normalen VehicleCard sichtbar.
const dealVehicles = homeVehicles.filter(v => !!vehicleDealMap[v.id]);
const dealsCount = (deals ? deals.length : 0) + dealVehicles.length;
// v2.2.13 — Höchster verfügbarer Rabatt für die Hero-Sektions-Headline,
// damit der Wert der Master-Aussage entspricht ("bis zu X% reduziert").
// v2.2.22 — Konsistent mit homeVehicles-Filter: nur Fahrzeuge, die auf der
// Startseite sichtbar sind, fließen in den Headline-Wert ein.
const maxDealPct = homeVehicles.reduce((acc, v) => {
const candidates = [];
if (v.oldPrice && v.oldPrice > v.pricePerDay && v.pricePerDay > 0) {
candidates.push(Math.round((1 - v.pricePerDay / v.oldPrice) * 100));
}
if (typeof v.dealPct === "number" && v.dealPct > 0) candidates.push(v.dealPct);
const tp = v.tierPrices || {};
const top = v.tierOldPrices || {};
["3", "6", "24", "168"].forEach(h => {
const price = h === "24" ? (tp[h] || v.pricePerDay) : tp[h];
const oldP = top[h];
if (price && oldP && Number(oldP) > 0 && Number(oldP) > Number(price)) {
candidates.push(Math.round((1 - Number(price) / Number(oldP)) * 100));
}
});
return candidates.length ? Math.max(acc, Math.max.apply(null, candidates)) : acc;
}, 0);
return (
{boot.heroLead || "Vom Alltagsauto bis zum AMG – Vollkasko inklusive, persönliche Übergabe bei Schaffhausen."}
{deal.subtitle}
Vom Alltagsauto über den 7-Plätzer und Transporter bis zum AMG —
wählen Sie Ihr Fahrzeug und fragen Sie in wenigen Klicks an.
Wählen Sie Fahrzeug und Mietdauer — der Preis erscheint sofort, transparent und inklusive MwSt.
Smart-Rabatte (Wochenende, längere Miete) werden automatisch angewendet.
Was wir versprechen, halten wir — von der Aufbereitung über faire
Preise bis zur persönlichen Betreuung.
{(boot.heroTitle || "Auto mieten in Neuhausen am Rheinfall").split("\n").map((line, i, arr) => (
}
{heroFeatures.map((f, i) => (
{deal.title}
{deal.subtitle && (
Alle Fahrzeuge auf einen Blick
Wofür brauchen Sie ein Auto?
Was kostet meine Wunsch-Miete?
Premium-Mieten, spürbar besser.