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:
+104
-6
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user