feat: implement Marco's customer changes

- Remove 'Flotte ansehen' button from hero section
- Remove '24/7 Support' stat from hero section
- Remove 'Unsere Flotte' eyebrow from fleet section
- Remove ALL 'Warum wir' / 'Why us' references from nav links, i18n keys, and legal pages
- Update reviews: Ferrari references only (removed GT3 mentions)
- Update Impressum with correct company data (MC Cars GmbH)
- Add multi-photo gallery: DB migration (17-vehicle-photos.sql), admin UI for photo management, frontend carousel on cards and dialog
- Update SEO: Ferrari-focused meta tags, title, keywords, JSON-LD
- Clean up dead i18n keys (viewFleet, statSupport, fleetEyebrow, navWhy, why* keys)
- Fix legal page issues: add config.js script, fix logo references to SVG
- Add Playwright E2E tests (26/26 passing)
- Update footer tagline across all pages
This commit is contained in:
2026-05-31 09:53:23 +02:00
parent e1f6bd56b0
commit 8be7d5aad2
18 changed files with 734 additions and 97 deletions
+104 -6
View File
@@ -16,6 +16,7 @@ const state = {
sort: "sort_order",
maxPrice: null,
reviewIdx: 0,
vehiclePhotosMap: new Map(),
};
// ---------------- Elements ----------------
@@ -124,6 +125,31 @@ async function loadVehicles() {
state.vehicles = data || [];
statCarsCount.textContent = state.vehicles.length;
// Load vehicle photos
if (state.vehicles.length > 0) {
const ids = state.vehicles.map(v => v.id);
const { data: photos } = await supabase
.from("vehicle_photos")
.select("*")
.in("vehicle_id", ids)
.order("display_order", { ascending: true });
state.vehiclePhotosMap = new Map();
if (photos) {
for (const ph of photos) {
if (!state.vehiclePhotosMap.has(ph.vehicle_id)) {
state.vehiclePhotosMap.set(ph.vehicle_id, []);
}
state.vehiclePhotosMap.get(ph.vehicle_id).push(ph);
}
}
// Also include legacy main photo if no gallery photos exist
for (const v of state.vehicles) {
if (!state.vehiclePhotosMap.has(v.id) && v.photo_url) {
state.vehiclePhotosMap.set(v.id, [{ photo_url: v.photo_url }]);
}
}
}
const brands = [...new Set(state.vehicles.map(v => v.brand))].sort();
brandFilter.innerHTML = `<option value="all">${t("all")}</option>` +
brands.map(b => `<option value="${b}">${b}</option>`).join("");
@@ -156,12 +182,16 @@ function renderGrid() {
emptyState.style.display = state.filtered.length ? "none" : "block";
for (const v of state.filtered) {
const photoUrl = optimizedVehiclePhotoUrl(v.photo_url);
const photos = state.vehiclePhotosMap?.get(v.id) || [];
const primaryPhoto = photos.find(p => p.is_primary) || photos[0];
const photoUrl = optimizedVehiclePhotoUrl(primaryPhoto?.photo_url || v.photo_url);
const photoCount = photos.length;
const card = document.createElement("article");
card.className = "vehicle-card";
card.innerHTML = `
<div class="vehicle-photo">
<img src="${escapeAttr(photoUrl)}" alt="${escapeAttr(v.brand)} ${escapeAttr(v.model)}" loading="lazy" decoding="async" />
<div class="vehicle-photo" data-photos='${escapeAttr(JSON.stringify(photos.map(p => optimizedVehiclePhotoUrl(p.photo_url))))}' data-current="0">
<img src="${escapeAttr(photoUrl)}" alt="${escapeAttr(v.brand)} ${escapeAttr(v.model)}" loading="lazy" decoding="async" class="vehicle-photo-img" />
${photoCount > 1 ? `<div class="vehicle-photo-nav"><button class="vehicle-photo-prev" aria-label="Vorheriges Foto"></button><button class="vehicle-photo-next" aria-label="Nächstes Foto"></button></div><div class="vehicle-photo-dots">${photos.map((_, i) => `<span class="${i === 0 ? 'active' : ''}"></span>`).join('')}</div>` : ''}
<span class="badge" aria-hidden="true">${escapeHtml(v.brand)}</span>
</div>
<div class="vehicle-body">
@@ -194,18 +224,54 @@ function renderGrid() {
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" });
});
});
// Photo carousel nav
grid.querySelectorAll(".vehicle-photo-prev").forEach(btn => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const container = btn.closest(".vehicle-photo");
const urls = JSON.parse(container.dataset.photos);
let idx = +container.dataset.current;
idx = (idx - 1 + urls.length) % urls.length;
container.dataset.current = idx;
container.querySelector(".vehicle-photo-img").src = urls[idx];
updatePhotoDots(container, idx);
});
});
grid.querySelectorAll(".vehicle-photo-next").forEach(btn => {
btn.addEventListener("click", (e) => {
e.stopPropagation();
const container = btn.closest(".vehicle-photo");
const urls = JSON.parse(container.dataset.photos);
let idx = +container.dataset.current;
idx = (idx + 1) % urls.length;
container.dataset.current = idx;
container.querySelector(".vehicle-photo-img").src = urls[idx];
updatePhotoDots(container, idx);
});
});
}
function updatePhotoDots(container, idx) {
container.querySelectorAll(".vehicle-photo-dots span").forEach((dot, i) => {
dot.classList.toggle("active", i === idx);
});
}
function openDetails(id) {
const v = state.vehicles.find(x => x.id === id);
if (!v) return;
const photoUrl = optimizedVehiclePhotoUrl(v.photo_url);
const photos = state.vehiclePhotosMap?.get(v.id) || [];
const photoUrls = photos.length ? photos.map(p => optimizedVehiclePhotoUrl(p.photo_url)) : [optimizedVehiclePhotoUrl(v.photo_url)];
const lang = getLang();
const desc = lang === "en" ? v.description_en : v.description_de;
dialogTitle.textContent = `${v.brand} ${v.model}`;
dialogBody.innerHTML = `
<img src="${escapeAttr(photoUrl)}" alt="${escapeAttr(v.brand + ' ' + v.model)}" />
<div class="dialog-gallery" data-gallery-urls='${escapeAttr(JSON.stringify(photoUrls))}' data-gallery-idx="0">
<img src="${escapeAttr(photoUrls[0])}" alt="${escapeAttr(v.brand + ' ' + v.model)}" class="dialog-gallery-main" />
${photoUrls.length > 1 ? `<div class="dialog-gallery-nav"><button class="dialog-gallery-prev" aria-label="Vorheriges Foto"></button><button class="dialog-gallery-next" aria-label="Nächstes Foto"></button></div><div class="dialog-gallery-thumbs">${photoUrls.map((u, i) => `<button class="${i === 0 ? 'active' : ''}" data-gidx="${i}"><img src="${escapeAttr(u)}" loading="lazy" /></button>`).join('')}</div>` : ''}
</div>
<p>${escapeHtml(desc || "")}</p>
<div class="spec-row" style="margin:1rem 0;">
<div><strong>${v.power_hp}</strong><span>${t("hp")}</span></div>
@@ -232,6 +298,37 @@ function openDetails(id) {
bpfCar.dispatchEvent(new Event("change"));
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" });
});
// Dialog gallery nav
const gallery = dialogBody.querySelector(".dialog-gallery");
const galleryPrev = dialogBody.querySelector(".dialog-gallery-prev");
const galleryNext = dialogBody.querySelector(".dialog-gallery-next");
if (galleryPrev) {
galleryPrev.addEventListener("click", () => {
let idx = +gallery.dataset.galleryIdx;
idx = (idx - 1 + photoUrls.length) % photoUrls.length;
gallery.dataset.galleryIdx = idx;
gallery.querySelector(".dialog-gallery-main").src = photoUrls[idx];
gallery.querySelectorAll(".dialog-gallery-thumbs button").forEach((b, i) => b.classList.toggle("active", i === idx));
});
}
if (galleryNext) {
galleryNext.addEventListener("click", () => {
let idx = +gallery.dataset.galleryIdx;
idx = (idx + 1) % photoUrls.length;
gallery.dataset.galleryIdx = idx;
gallery.querySelector(".dialog-gallery-main").src = photoUrls[idx];
gallery.querySelectorAll(".dialog-gallery-thumbs button").forEach((b, i) => b.classList.toggle("active", i === idx));
});
}
gallery?.querySelectorAll(".dialog-gallery-thumbs button").forEach(btn => {
btn.addEventListener("click", () => {
const idx = +btn.dataset.gidx;
gallery.dataset.galleryIdx = idx;
gallery.querySelector(".dialog-gallery-main").src = photoUrls[idx];
gallery.querySelectorAll(".dialog-gallery-thumbs button").forEach((b, i) => b.classList.toggle("active", i === idx));
});
});
}
// ---------------- Reviews ----------------
@@ -400,7 +497,8 @@ async function updateSidebar() {
const deposit = price.deposit_eur;
const includedKmPerDay = price.included_km_per_day || 150;
const includedKm = totalDays * includedKmPerDay;
const photoUrl = optimizedVehiclePhotoUrl(v.photo_url);
const sidebarPhotos = state.vehiclePhotosMap?.get(v.id) || [];
const photoUrl = optimizedVehiclePhotoUrl((sidebarPhotos.find(p => p.is_primary) || sidebarPhotos[0] || v)?.photo_url || v.photo_url);
if (totalDays > 2) {
// Individuell mode: show info banner instead of pricing