Compare commits

..

1 Commits

Author SHA1 Message Date
Lago c6ee8cd927 fix: use singular 'Fahrzeug' when only 1 car available 2026-05-31 14:07:23 +02:00
19 changed files with 151 additions and 1407 deletions
-1
View File
@@ -31,7 +31,6 @@ services:
- ./supabase/migrations/14-email-requested-trigger.sql:/sql/14-email-requested-trigger.sql:ro - ./supabase/migrations/14-email-requested-trigger.sql:/sql/14-email-requested-trigger.sql:ro
- ./supabase/migrations/15-individuell-vat-subtotal-fix.sql:/sql/15-individuell-vat-subtotal-fix.sql:ro - ./supabase/migrations/15-individuell-vat-subtotal-fix.sql:/sql/15-individuell-vat-subtotal-fix.sql:ro
- ./supabase/migrations/16-rental-type-weekend-gap-fix.sql:/sql/16-rental-type-weekend-gap-fix.sql:ro - ./supabase/migrations/16-rental-type-weekend-gap-fix.sql:/sql/16-rental-type-weekend-gap-fix.sql:ro
- ./supabase/migrations/17-vehicle-photos.sql:/sql/17-vehicle-photos.sql:ro
kong: kong:
volumes: volumes:
-2
View File
@@ -224,7 +224,6 @@ services:
- /mnt/user/appdata/mc-cars/supabase/migrations/14-email-requested-trigger.sql:/sql/14-email-requested-trigger.sql:ro - /mnt/user/appdata/mc-cars/supabase/migrations/14-email-requested-trigger.sql:/sql/14-email-requested-trigger.sql:ro
- /mnt/user/appdata/mc-cars/supabase/migrations/15-individuell-vat-subtotal-fix.sql:/sql/15-individuell-vat-subtotal-fix.sql:ro - /mnt/user/appdata/mc-cars/supabase/migrations/15-individuell-vat-subtotal-fix.sql:/sql/15-individuell-vat-subtotal-fix.sql:ro
- /mnt/user/appdata/mc-cars/supabase/migrations/16-rental-type-weekend-gap-fix.sql:/sql/16-rental-type-weekend-gap-fix.sql:ro - /mnt/user/appdata/mc-cars/supabase/migrations/16-rental-type-weekend-gap-fix.sql:/sql/16-rental-type-weekend-gap-fix.sql:ro
- /mnt/user/appdata/mc-cars/supabase/migrations/17-vehicle-photos.sql:/sql/17-vehicle-photos.sql:ro
entrypoint: ["sh","-c"] entrypoint: ["sh","-c"]
command: command:
- | - |
@@ -257,7 +256,6 @@ services:
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/14-email-requested-trigger.sql psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/14-email-requested-trigger.sql
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/15-individuell-vat-subtotal-fix.sql psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/15-individuell-vat-subtotal-fix.sql
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/16-rental-type-weekend-gap-fix.sql psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/16-rental-type-weekend-gap-fix.sql
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/17-vehicle-photos.sql
echo "post-init done." echo "post-init done."
restart: "no" restart: "no"
networks: [mccars] networks: [mccars]
+5 -10
View File
@@ -170,18 +170,13 @@
<form class="admin-form" id="vehicleForm"> <form class="admin-form" id="vehicleForm">
<input type="hidden" name="vid" /> <input type="hidden" name="vid" />
<div class="admin-photo-upload-zone" id="photoUploadZone"> <div class="admin-photo-preview" id="photoPreview"></div>
<div class="admin-photo-upload-content"> <label>
<span class="admin-photo-upload-icon">📷</span> <span data-i18n="adminPhotoUpload">Foto hochladen (JPG/PNG/WebP, max 50 MB)</span>
<span>Fotos hochladen (JPG/PNG/WebP, max 50 MB)</span> <input type="file" id="photoInput" accept="image/*" />
<span class="muted" style="font-size:0.85rem;">Klicken oder Dateien hierher ziehen · Mehrfachauswahl möglich</span> </label>
</div>
<input type="file" id="photoInput" accept="image/*" multiple />
</div>
<input type="hidden" name="photo_url" /> <input type="hidden" name="photo_url" />
<div class="admin-photo-gallery" id="extraPhotoGallery"></div>
<div class="row2"> <div class="row2">
<label><span data-i18n="adminBrand">Marke</span><input name="brand" required /></label> <label><span data-i18n="adminBrand">Marke</span><input name="brand" required /></label>
<label><span data-i18n="adminModel">Modell</span><input name="model" required /></label> <label><span data-i18n="adminModel">Modell</span><input name="model" required /></label>
+18 -254
View File
@@ -52,8 +52,7 @@ const formTitle = document.querySelector("#formTitle");
const saveBtn = document.querySelector("#saveBtn"); const saveBtn = document.querySelector("#saveBtn");
const resetBtn = document.querySelector("#resetBtn"); const resetBtn = document.querySelector("#resetBtn");
const photoInput = document.querySelector("#photoInput"); const photoInput = document.querySelector("#photoInput");
const photoUploadZone = document.querySelector("#photoUploadZone"); const photoPreview = document.querySelector("#photoPreview");
const extraPhotoGallery = document.querySelector("#extraPhotoGallery");
const tableBody = document.querySelector("#adminTable tbody"); const tableBody = document.querySelector("#adminTable tbody");
// ----- State ----- // ----- State -----
@@ -67,7 +66,6 @@ const state = {
vehicles: [], vehicles: [],
vehicleMap: new Map(), vehicleMap: new Map(),
currentPhotoPath: null, currentPhotoPath: null,
vehiclePhotos: [],
realtimeChannel: null, realtimeChannel: null,
forcedRotation: false, forcedRotation: false,
}; };
@@ -323,7 +321,7 @@ function loadForEdit(id) {
vehicleForm.photo_url.value = v.photo_url; vehicleForm.photo_url.value = v.photo_url;
vehicleForm.is_active.checked = v.is_active; vehicleForm.is_active.checked = v.is_active;
state.currentPhotoPath = v.photo_path || null; state.currentPhotoPath = v.photo_path || null;
loadVehiclePhotos(v.id); updatePreview(v.photo_url);
window.scrollTo({ top: 0, behavior: "smooth" }); window.scrollTo({ top: 0, behavior: "smooth" });
} }
@@ -339,7 +337,7 @@ resetBtn.addEventListener("click", () => {
vehicleForm.kaution_eur.value = 5000; vehicleForm.kaution_eur.value = 5000;
vehicleForm.price_per_km_eur.value = 1.50; vehicleForm.price_per_km_eur.value = 1.50;
state.currentPhotoPath = null; state.currentPhotoPath = null;
state.vehiclePhotos = []; updatePreview("");
formTitle.textContent = "Neues Fahrzeug"; formTitle.textContent = "Neues Fahrzeug";
formFeedback.textContent = ""; formFeedback.textContent = "";
}); });
@@ -392,275 +390,41 @@ async function deleteVehicle(id) {
const v = state.vehicleMap.get(id); const v = state.vehicleMap.get(id);
if (!v) return; if (!v) return;
if (!confirm(`Delete ${v.brand} ${v.model}?`)) return; if (!confirm(`Delete ${v.brand} ${v.model}?`)) return;
// Delete old main photo
if (v.photo_path) await supabase.storage.from("vehicle-photos").remove([v.photo_path]); if (v.photo_path) await supabase.storage.from("vehicle-photos").remove([v.photo_path]);
// Delete gallery photos from storage
const { data: photos } = await supabase.from("vehicle_photos").select("photo_path").eq("vehicle_id", id);
if (photos?.length) {
await supabase.storage.from("vehicle-photos").remove(photos.map(p => p.photo_path));
}
const { error } = await supabase.from("vehicles").delete().eq("id", id); const { error } = await supabase.from("vehicles").delete().eq("id", id);
if (error) { alert(error.message); return; } if (error) { alert(error.message); return; }
await loadVehicles(); await loadVehicles();
renderVehicles(); renderVehicles();
} }
// ----- Unified Photo Upload + Gallery ----- // Photo upload
async function loadVehiclePhotos(vehicleId) {
if (!vehicleId) {
state.vehiclePhotos = [];
renderExtraPhotoGallery();
return;
}
const { data, error } = await supabase
.from("vehicle_photos")
.select("*")
.eq("vehicle_id", vehicleId)
.order("display_order", { ascending: true });
if (error) { console.error("Failed to load vehicle photos:", error); return; }
state.vehiclePhotos = data || [];
renderExtraPhotoGallery();
}
function renderExtraPhotoGallery() {
if (!extraPhotoGallery) return;
extraPhotoGallery.innerHTML = "";
const photos = state.vehiclePhotos;
if (!photos.length) return;
for (let i = 0; i < photos.length; i++) {
const ph = photos[i];
const card = document.createElement("div");
card.className = "admin-photo-card";
card.draggable = true;
card.dataset.photoId = ph.id;
card.dataset.photoIdx = i;
card.innerHTML = `
<img src="${attr(ph.photo_url)}" alt="Foto ${i + 1}" />
<div class="admin-photo-card-arrows">
<button type="button" class="admin-photo-arrow" data-move-dir="-1" ${i === 0 ? 'disabled style="opacity:0.3;pointer-events:none;"' : ''} aria-label="Nach links verschieben"></button>
<button type="button" class="admin-photo-arrow" data-move-dir="1" ${i === photos.length - 1 ? 'disabled style="opacity:0.3;pointer-events:none;"' : ''} aria-label="Nach rechts verschieben"></button>
</div>
<div class="admin-photo-card-actions">
${!ph.is_primary ? `<button type="button" class="admin-photo-set-primary" data-photo-id="${ph.id}" aria-label="Als Hauptfoto setzen">★</button>` : ''}
<button type="button" class="admin-photo-delete" data-photo-id="${ph.id}" aria-label="Foto löschen">×</button>
</div>
${ph.is_primary ? '<span class="admin-photo-badge">Hauptfoto</span>' : ''}
<span class="admin-photo-drag-handle" aria-hidden="true">⠿</span>
`;
extraPhotoGallery.appendChild(card);
}
// Action buttons
extraPhotoGallery.querySelectorAll(".admin-photo-delete").forEach(btn => {
btn.addEventListener("click", async () => {
await deleteVehiclePhoto(btn.dataset.photoId);
});
});
extraPhotoGallery.querySelectorAll(".admin-photo-set-primary").forEach(btn => {
btn.addEventListener("click", async () => {
await setPrimaryPhoto(btn.dataset.photoId);
});
});
extraPhotoGallery.querySelectorAll(".admin-photo-arrow").forEach(btn => {
btn.addEventListener("click", async () => {
const card = btn.closest(".admin-photo-card");
const idx = +card.dataset.photoIdx;
const dir = +btn.dataset.moveDir;
await reorderPhoto(idx, dir);
});
});
// Drag and drop
extraPhotoGallery.querySelectorAll(".admin-photo-card").forEach(card => {
card.addEventListener("dragstart", handleDragStart);
card.addEventListener("dragover", handleDragOver);
card.addEventListener("dragenter", handleDragEnter);
card.addEventListener("dragleave", handleDragLeave);
card.addEventListener("drop", handleDrop);
card.addEventListener("dragend", handleDragEnd);
});
}
let draggedPhotoIdx = null;
function handleDragStart(e) {
draggedPhotoIdx = +this.dataset.photoIdx;
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", this.dataset.photoId);
this.style.opacity = "0.4";
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
}
function handleDragEnter(e) {
e.preventDefault();
if (+this.dataset.photoIdx !== draggedPhotoIdx) {
this.classList.add("admin-photo-card-drag-over");
}
}
function handleDragLeave() {
this.classList.remove("admin-photo-card-drag-over");
}
function handleDrop(e) {
e.preventDefault();
this.classList.remove("admin-photo-card-drag-over");
const targetIdx = +this.dataset.photoIdx;
if (draggedPhotoIdx !== null && draggedPhotoIdx !== targetIdx) {
const dir = targetIdx > draggedPhotoIdx ? 1 : -1;
reorderPhoto(draggedPhotoIdx, dir, targetIdx);
}
}
function handleDragEnd() {
this.style.opacity = "1";
draggedPhotoIdx = null;
extraPhotoGallery?.querySelectorAll(".admin-photo-card").forEach(c => c.classList.remove("admin-photo-card-drag-over"));
}
async function reorderPhoto(fromIdx, dir, targetIdx) {
const photos = state.vehiclePhotos;
if (photos.length < 2) return;
let toIdx;
if (targetIdx !== undefined) {
toIdx = targetIdx;
} else {
toIdx = fromIdx + dir;
if (toIdx < 0 || toIdx >= photos.length) return;
}
// Swap in local array
[photos[fromIdx], photos[toIdx]] = [photos[toIdx], photos[fromIdx]];
// Build order payload
const orderPayload = photos.map((p, i) => ({ id: p.id, order: i }));
const vid = vehicleForm.vid?.value;
if (vid) {
try {
await supabase.rpc("reorder_vehicle_photos", {
p_vehicle_id: vid,
p_photo_orders: orderPayload,
});
} catch (err) {
console.error("Reorder failed:", err);
[photos[fromIdx], photos[toIdx]] = [photos[fromIdx], photos[toIdx]];
return;
}
}
renderExtraPhotoGallery();
}
async function deleteVehiclePhoto(photoId) {
const ph = state.vehiclePhotos.find(p => p.id === photoId);
if (!ph) return;
try {
if (ph.photo_path) {
await supabase.storage.from("vehicle-photos").remove([ph.photo_path]);
}
const { error } = await supabase.from("vehicle_photos").delete().eq("id", photoId);
if (error) throw error;
state.vehiclePhotos = state.vehiclePhotos.filter(p => p.id !== photoId);
// If deleted was primary, promote next photo
if (ph.is_primary && state.vehiclePhotos.length) {
const newPrimary = state.vehiclePhotos[0];
const vid = vehicleForm.vid?.value;
if (vid) {
await supabase.rpc("set_primary_vehicle_photo", { p_vehicle_id: vid, p_photo_id: newPrimary.id });
vehicleForm.photo_url.value = newPrimary.photo_url;
}
}
renderExtraPhotoGallery();
} catch (err) {
console.error("Failed to delete photo:", err);
}
}
async function setPrimaryPhoto(photoId) {
const vid = vehicleForm.vid?.value;
if (!vid) return;
try {
const ph = state.vehiclePhotos.find(p => p.id === photoId);
await supabase.rpc("set_primary_vehicle_photo", { p_vehicle_id: vid, p_photo_id: photoId });
vehicleForm.photo_url.value = ph?.photo_url || "";
await loadVehiclePhotos(vid);
} catch (err) {
console.error("Failed to set primary photo:", err);
}
}
// Unified photo upload handler
photoInput.addEventListener("change", async () => { photoInput.addEventListener("change", async () => {
const files = photoInput.files; const file = photoInput.files?.[0];
if (!files.length) return; if (!file) return;
const vid = vehicleForm.vid?.value;
if (!vid) {
formFeedback.className = "form-feedback error";
formFeedback.textContent = "Bitte zuerst das Fahrzeug speichern, dann Fotos hinzufügen.";
return;
}
formFeedback.className = "form-feedback"; formFeedback.className = "form-feedback";
formFeedback.textContent = `Uploading ${files.length} photo(s)...`; formFeedback.textContent = "Uploading photo...";
let uploaded = 0;
for (const file of files) {
try { try {
// Delete old photo if exists
if (state.currentPhotoPath) {
await supabase.storage.from("vehicle-photos").remove([state.currentPhotoPath]);
}
const ext = (file.name.split(".").pop() || "jpg").toLowerCase(); const ext = (file.name.split(".").pop() || "jpg").toLowerCase();
const path = `${vid}/${crypto.randomUUID()}.${ext}`; const path = `${crypto.randomUUID()}.${ext}`;
const { error: upErr } = await supabase.storage const { error: upErr } = await supabase.storage
.from("vehicle-photos") .from("vehicle-photos")
.upload(path, file, { contentType: file.type, upsert: true }); .upload(path, file, { contentType: file.type, upsert: true });
if (upErr) throw upErr; if (upErr) throw upErr;
const { data: pub } = supabase.storage.from("vehicle-photos").getPublicUrl(path); const { data: pub } = supabase.storage.from("vehicle-photos").getPublicUrl(path);
const isFirst = state.vehiclePhotos.length === 0; state.currentPhotoPath = path;
const maxOrder = state.vehiclePhotos.reduce((m, p) => Math.max(m, p.display_order), -1);
await supabase.from("vehicle_photos").insert({
vehicle_id: vid,
photo_url: pub.publicUrl,
photo_path: path,
display_order: maxOrder + 1,
is_primary: isFirst,
});
if (isFirst) {
vehicleForm.photo_url.value = pub.publicUrl; vehicleForm.photo_url.value = pub.publicUrl;
} updatePreview(pub.publicUrl);
uploaded++; formFeedback.textContent = "Upload ok.";
} catch (err) { } catch (err) {
console.error("Upload failed:", err); formFeedback.className = "form-feedback error";
} formFeedback.textContent = err.message || String(err);
}
await loadVehiclePhotos(vid);
formFeedback.textContent = `${uploaded} Foto(s) hochgeladen.`;
photoInput.value = "";
});
// Drag-and-drop on upload zone
if (photoUploadZone) {
photoUploadZone.addEventListener("dragover", e => {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
photoUploadZone.classList.add("drag-active");
});
photoUploadZone.addEventListener("dragleave", () => {
photoUploadZone.classList.remove("drag-active");
});
photoUploadZone.addEventListener("drop", e => {
e.preventDefault();
photoUploadZone.classList.remove("drag-active");
const files = e.dataTransfer.files;
if (files.length) {
photoInput.files = files;
photoInput.dispatchEvent(new Event("change"));
} }
}); });
} function updatePreview(url) { photoPreview.style.backgroundImage = url ? `url('${url}')` : ""; }
// ========================================================================= // =========================================================================
// LEADS // LEADS
+12 -9
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AGB · MC Cars</title> <title>AGB · MC Cars</title>
<link rel="icon" type="image/svg+xml" href="/images/MC-Cars-Logo.svg" /> <link rel="icon" type="image/png" href="/images/mc-cars-logo.png" />
<link rel="apple-touch-icon" href="/images/MC-Cars-Logo.svg" /> <link rel="apple-touch-icon" href="/images/mc-cars-logo.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;600;700&display=swap" rel="stylesheet" />
@@ -51,12 +51,13 @@
<header class="site-header"> <header class="site-header">
<div class="shell"> <div class="shell">
<a class="logo" href="/" aria-label="MC Cars Startseite"> <a class="logo" href="/" aria-label="MC Cars Startseite">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</a> </a>
<button class="menu-toggle" aria-label="Menü"></button> <button class="menu-toggle" aria-label="Menü"></button>
<nav class="main-nav" aria-label="Hauptnavigation"> <nav class="main-nav" aria-label="Hauptnavigation">
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#stimmen" data-i18n="navReviews">Stimmen</a> <a href="/#stimmen" data-i18n="navReviews">Stimmen</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
<a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a> <a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a>
@@ -76,7 +77,7 @@
Die AGB definieren die rechtlichen Bedingungen für die Vermietung von Fahrzeugen durch MC Cars. Die AGB definieren die rechtlichen Bedingungen für die Vermietung von Fahrzeugen durch MC Cars.
</p> </p>
<p> <p>
Bitte wenden Sie sich an hello@mc-cars.at für weitere Informationen. Bitte wenden Sie sich an hello@mccars.at für weitere Informationen.
</p> </p>
</div> </div>
</div> </div>
@@ -87,15 +88,16 @@
<div class="footer-grid"> <div class="footer-grid">
<div> <div>
<div class="logo" style="margin-bottom:0.8rem;"> <div class="logo" style="margin-bottom:0.8rem;">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</div> </div>
<p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in der Steiermark, Österreich.</p> <p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).</p>
</div> </div>
<div> <div>
<h4 data-i18n="footerNav">Navigation</h4> <h4 data-i18n="footerNav">Navigation</h4>
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
</div> </div>
@@ -109,7 +111,8 @@
<div> <div>
<h4 data-i18n="footerContact">Kontakt</h4> <h4 data-i18n="footerContact">Kontakt</h4>
<a href="mailto:hello@mc-cars.at">hello@mc-cars.at</a> <a href="mailto:hello@mccars.at">hello@mccars.at</a>
<a href="tel:+43316880000">+43 316 880000</a>
</div> </div>
</div> </div>
+7 -105
View File
@@ -16,7 +16,6 @@ const state = {
sort: "sort_order", sort: "sort_order",
maxPrice: null, maxPrice: null,
reviewIdx: 0, reviewIdx: 0,
vehiclePhotosMap: new Map(),
}; };
// ---------------- Elements ---------------- // ---------------- Elements ----------------
@@ -124,34 +123,9 @@ async function loadVehicles() {
state.vehicles = data || []; state.vehicles = data || [];
statCarsCount.textContent = state.vehicles.length; statCarsCount.textContent = state.vehicles.length;
statCarsLabel.dataset.i18n = state.vehicles.length > 1 ? 'statCars' : 'statCar'; statCarsLabel.dataset.i18n = state.vehicles.length === 1 ? 'statCar' : 'statCars';
applyI18n(); applyI18n();
// 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(); const brands = [...new Set(state.vehicles.map(v => v.brand))].sort();
brandFilter.innerHTML = `<option value="all">${t("all")}</option>` + brandFilter.innerHTML = `<option value="all">${t("all")}</option>` +
brands.map(b => `<option value="${b}">${b}</option>`).join(""); brands.map(b => `<option value="${b}">${b}</option>`).join("");
@@ -184,16 +158,12 @@ function renderGrid() {
emptyState.style.display = state.filtered.length ? "none" : "block"; emptyState.style.display = state.filtered.length ? "none" : "block";
for (const v of state.filtered) { for (const v of state.filtered) {
const photos = state.vehiclePhotosMap?.get(v.id) || []; const photoUrl = optimizedVehiclePhotoUrl(v.photo_url);
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"); const card = document.createElement("article");
card.className = "vehicle-card"; card.className = "vehicle-card";
card.innerHTML = ` card.innerHTML = `
<div class="vehicle-photo" data-photos='${escapeAttr(JSON.stringify(photos.map(p => optimizedVehiclePhotoUrl(p.photo_url))))}' data-current="0"> <div class="vehicle-photo">
<img src="${escapeAttr(photoUrl)}" alt="${escapeAttr(v.brand)} ${escapeAttr(v.model)}" loading="lazy" decoding="async" class="vehicle-photo-img" /> <img src="${escapeAttr(photoUrl)}" alt="${escapeAttr(v.brand)} ${escapeAttr(v.model)}" loading="lazy" decoding="async" />
${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> <span class="badge" aria-hidden="true">${escapeHtml(v.brand)}</span>
</div> </div>
<div class="vehicle-body"> <div class="vehicle-body">
@@ -226,54 +196,18 @@ function renderGrid() {
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" }); 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) { function openDetails(id) {
const v = state.vehicles.find(x => x.id === id); const v = state.vehicles.find(x => x.id === id);
if (!v) return; if (!v) return;
const photos = state.vehiclePhotosMap?.get(v.id) || []; const photoUrl = optimizedVehiclePhotoUrl(v.photo_url);
const photoUrls = photos.length ? photos.map(p => optimizedVehiclePhotoUrl(p.photo_url)) : [optimizedVehiclePhotoUrl(v.photo_url)];
const lang = getLang(); const lang = getLang();
const desc = lang === "en" ? v.description_en : v.description_de; const desc = lang === "en" ? v.description_en : v.description_de;
dialogTitle.textContent = `${v.brand} ${v.model}`; dialogTitle.textContent = `${v.brand} ${v.model}`;
dialogBody.innerHTML = ` dialogBody.innerHTML = `
<div class="dialog-gallery" data-gallery-urls='${escapeAttr(JSON.stringify(photoUrls))}' data-gallery-idx="0"> <img src="${escapeAttr(photoUrl)}" alt="${escapeAttr(v.brand + ' ' + v.model)}" />
<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> <p>${escapeHtml(desc || "")}</p>
<div class="spec-row" style="margin:1rem 0;"> <div class="spec-row" style="margin:1rem 0;">
<div><strong>${v.power_hp}</strong><span>${t("hp")}</span></div> <div><strong>${v.power_hp}</strong><span>${t("hp")}</span></div>
@@ -300,37 +234,6 @@ function openDetails(id) {
bpfCar.dispatchEvent(new Event("change")); bpfCar.dispatchEvent(new Event("change"));
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" }); 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 ---------------- // ---------------- Reviews ----------------
@@ -499,8 +402,7 @@ async function updateSidebar() {
const deposit = price.deposit_eur; const deposit = price.deposit_eur;
const includedKmPerDay = price.included_km_per_day || 150; const includedKmPerDay = price.included_km_per_day || 150;
const includedKm = totalDays * includedKmPerDay; const includedKm = totalDays * includedKmPerDay;
const sidebarPhotos = state.vehiclePhotosMap?.get(v.id) || []; const photoUrl = optimizedVehiclePhotoUrl(v.photo_url);
const photoUrl = optimizedVehiclePhotoUrl((sidebarPhotos.find(p => p.is_primary) || sidebarPhotos[0] || v)?.photo_url || v.photo_url);
if (totalDays > 2) { if (totalDays > 2) {
// Individuell mode: show info banner instead of pricing // Individuell mode: show info banner instead of pricing
+16 -98
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Datenschutz · MC Cars (GmbH)</title> <title>Datenschutz · MC Cars (GmbH)</title>
<link rel="icon" type="image/svg+xml" href="/images/MC-Cars-Logo.svg" /> <link rel="icon" type="image/png" href="/images/mc-cars-logo.png" />
<link rel="apple-touch-icon" href="/images/MC-Cars-Logo.svg" /> <link rel="apple-touch-icon" href="/images/mc-cars-logo.png" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
@@ -48,12 +48,13 @@
<header class="site-header"> <header class="site-header">
<div class="shell"> <div class="shell">
<a class="logo" href="/" aria-label="MC Cars Startseite"> <a class="logo" href="/" aria-label="MC Cars Startseite">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</a> </a>
<button class="menu-toggle" aria-label="Menü"></button> <button class="menu-toggle" aria-label="Menü"></button>
<nav class="main-nav" aria-label="Hauptnavigation"> <nav class="main-nav" aria-label="Hauptnavigation">
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#stimmen" data-i18n="navReviews">Stimmen</a> <a href="/#stimmen" data-i18n="navReviews">Stimmen</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
<a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a> <a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a>
@@ -63,95 +64,11 @@
</header> </header>
<main style="padding: 3rem 0;"> <main style="padding: 3rem 0;">
<div class="shell" style="max-width: 80ch;"> <div class="shell">
<h1>Datenschutzerklärung</h1> <h1>Datenschutz</h1>
<div style="max-width: 65ch; line-height: 1.7; color: var(--text);">
<div style="max-width: 70ch; line-height: 1.8; color: var(--text);"> <p>Buchungsanfragen werden aktuell zu Demozwecken lokal im Browser gespeichert. Fahrzeugdaten werden über ein selbstgehostetes Supabase verwaltet.</p>
<p>Der Schutz Ihrer persönlichen Daten ist uns ein wichtiges Anliegen. Wir verarbeiten Ihre Daten daher ausschließlich auf Grundlage der gesetzlichen Bestimmungen (DSGVO, DSG 2018). In diesen Datenschutzinformationen informieren wir Sie über die wichtigsten Aspekte der Datenverarbeitung im Rahmen unserer Website.</p> <p>Ansprechpartner: hello@mccars.at</p>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Verantwortlicher für die Datenverarbeitung</h2>
<p><strong>MC Cars GmbH</strong><br/> Gaisfeld 1/2, 8564 Krottendorf-Gaisfeld<br/> E-Mail: hello@mc-cars.at</p>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Daten, die wir verarbeiten</h2>
<h3 style="font-size: 1.05rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.3rem;">Server-Logfiles</h3>
<p>Beim Besuch unserer Website werden automatisch Informationen in Server-Logfiles gespeichert, die Ihr Browser an uns übermittelt. Dies sind:</p>
<ul style="padding-left: 1.5rem;">
<li>Browsertyp und Browserversion</li>
<li>Verwendetes Betriebssystem</li>
<li>Referrer URL (die zuvor besuchte Seite)</li>
<li>Hostname des zugreifenden Rechners</li>
<li>Uhrzeit der Serveranfrage</li>
<li>IP-Adresse</li>
</ul>
<p>Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.</p>
<h3 style="font-size: 1.05rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.3rem;">Buchungsanfragen</h3>
<p>Wenn Sie unser Buchungsformular nutzen, werden Ihre angegebenen Daten zwecks Bearbeitung der Anfrage und für den Fall von Anschlussfragen gespeichert. Dies umfasst:</p>
<ul style="padding-left: 1.5rem;">
<li>Name</li>
<li>E-Mail-Adresse</li>
<li>Telefonnummer</li>
<li>Gewähltes Fahrzeug und Mietzeitraum</li>
<li>Nachricht / Anmerkungen</li>
</ul>
<p>Diese Daten geben wir nicht ohne Ihre Einwilligung weiter.</p>
<h3 style="font-size: 1.05rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.3rem;">Identitätsdokumente</h3>
<p>Zur Bearbeitung von Buchungsanfragen laden wir Identitätsdokumente (Ausweis, Führerschein) sowie optionale Einkommensnachweise hoch. Diese Dokumente dienen ausschließlich der Identitätsverifizierung und Bonitätsprüfung. Sie werden vertraulich behandelt und nicht an Dritte weitergegeben.</p>
<h3 style="font-size: 1.05rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.3rem;">Cookies und lokale Speicherung</h3>
<p>Unsere Website verwendet lokale Speicherung (localStorage) für die Auswahl der Spracheinstellung. Diese Daten werden ausschließlich auf Ihrem Endgerät gespeichert und nicht an uns übermittelt.</p>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Zweck der Datenverarbeitung</h2>
<p>Die Verarbeitung Ihrer personenbezogenen Daten erfolgt zu folgenden Zwecken:</p>
<ul style="padding-left: 1.5rem;">
<li>Zur Bereitstellung, Optimierung und Weiterentwicklung unserer Website</li>
<li>Zur Bearbeitung Ihrer Buchungsanfragen</li>
<li>Zur Identitätsprüfung und Bonitätsprüfung</li>
<li>Zur Gewährleistung der Sicherheit und Funktionsfähigkeit unserer Website</li>
<li>Zur Erfüllung gesetzlicher Verpflichtungen</li>
</ul>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Rechtsgrundlage der Verarbeitung</h2>
<p>Die Verarbeitung Ihrer personenbezogenen Daten erfolgt auf folgenden Rechtsgrundlagen:</p>
<ul style="padding-left: 1.5rem;">
<li><strong>Erfüllung eines Vertrags oder vorvertraglicher Maßnahmen (Art. 6 Abs. 1 lit. b DSGVO)</strong> bei der Bearbeitung Ihrer Buchungsanfragen und der Verarbeitung Ihrer Identitätsdokumente</li>
<li><strong>Erfüllung einer rechtlichen Verpflichtung (Art. 6 Abs. 1 lit. c DSGVO)</strong> z.B. aufgrund gesetzlicher Aufbewahrungsfristen</li>
<li><strong>Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO)</strong> zur Gewährleistung der Sicherheit, der Funktionsfähigkeit und der Optimierung unserer Website</li>
</ul>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Datenhosting</h2>
<p>Unsere Website und Datenbank laufen auf einer selbstgehosteten Infrastruktur. Alle personenbezogenen Daten werden auf unseren eigenen Servern verarbeitet und gespeichert. Es erfolgt keine Weitergabe an Cloud-Dienstanbieter oder Drittunternehmen.</p>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Übermittlung Ihrer Daten</h2>
<p>Eine Übermittlung Ihrer personenbezogenen Daten an Dritte erfolgt grundsätzlich nicht, es sei denn:</p>
<ul style="padding-left: 1.5rem;">
<li>Dies ist zur Erfüllung unserer vertraglichen Pflichten erforderlich</li>
<li>Wir sind gesetzlich dazu verpflichtet</li>
<li>Sie haben ausdrücklich eingewilligt</li>
</ul>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Speicherdauer</h2>
<p>Wir speichern Ihre personenbezogenen Daten nur so lange, wie es für die Erreichung der oben genannten Zwecke erforderlich ist oder wie es die gesetzlichen Aufbewahrungspflichten vorsehen. Identitätsdokumente werden nach Abschluss der Buchung und Erfüllung der gesetzlichen Aufbewahrungsfristen gelöscht.</p>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Ihre Rechte</h2>
<p>Sie haben hinsichtlich Ihrer bei uns gespeicherten personenbezogenen Daten folgende Rechte:</p>
<ul style="padding-left: 1.5rem;">
<li><strong>Recht auf Auskunft (Art. 15 DSGVO):</strong> Sie können Auskunft darüber verlangen, ob und welche personenbezogenen Daten von Ihnen verarbeitet werden.</li>
<li><strong>Recht auf Berichtigung (Art. 16 DSGVO):</strong> Sie können die Berichtigung unrichtiger oder die Vervollständigung unvollständiger Daten verlangen.</li>
<li><strong>Recht auf Löschung (Art. 17 DSGVO):</strong> Sie können die Löschung Ihrer Daten verlangen, sofern die gesetzlichen Voraussetzungen dafür vorliegen.</li>
<li><strong>Recht auf Einschränkung der Verarbeitung (Art. 18 DSGVO):</strong> Sie können die Einschränkung der Verarbeitung Ihrer Daten verlangen, sofern die gesetzlichen Voraussetzungen dafür vorliegen.</li>
<li><strong>Recht auf Datenübertragbarkeit (Art. 20 DSGVO):</strong> Sie haben das Recht, Ihre bereitgestellten Daten in einem strukturierten, gängigen und maschinenlesbaren Format zu erhalten.</li>
<li><strong>Recht auf Widerspruch (Art. 21 DSGVO):</strong> Sie können gegen die Verarbeitung Ihrer Daten Widerspruch einlegen.</li>
<li><strong>Recht auf Beschwerde (Art. 77 DSGVO):</strong> Sie können sich bei der zuständigen Aufsichtsbehörde beschweren.</li>
</ul>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Kontaktdaten der Aufsichtsbehörde</h2>
<p><strong>Österreichische Datenschutzbehörde</strong><br/> Barichgasse 40-42, 1030 Wien, Österreich<br/> Telefon: +43 1 52 152-0<br/> E-Mail: <a href="mailto:dsb@dsb.gv.at" style="color: var(--accent-strong);">dsb@dsb.gv.at</a></p>
<h2 style="font-size: 1.25rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.5rem; color: var(--text);">Änderungen dieser Datenschutzerklärung</h2>
<p>Wir behalten uns vor, diese Datenschutzerklärung anzupassen, um sie an geänderte Rechtslagen oder bei Änderungen unserer Dienste anzupassen. Die jeweils aktuelle Version ist auf unserer Website abrufbar.</p>
</div> </div>
</div> </div>
</main> </main>
@@ -161,15 +78,16 @@
<div class="footer-grid"> <div class="footer-grid">
<div> <div>
<div class="logo" style="margin-bottom:0.8rem;"> <div class="logo" style="margin-bottom:0.8rem;">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</div> </div>
<p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in der Steiermark, Österreich.</p> <p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).</p>
</div> </div>
<div> <div>
<h4 data-i18n="footerNav">Navigation</h4> <h4 data-i18n="footerNav">Navigation</h4>
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
</div> </div>
@@ -183,7 +101,8 @@
<div> <div>
<h4 data-i18n="footerContact">Kontakt</h4> <h4 data-i18n="footerContact">Kontakt</h4>
<a href="mailto:hello@mc-cars.at">hello@mc-cars.at</a> <a href="mailto:hello@mccars.at">hello@mccars.at</a>
<a href="tel:+43316880000">+43 316 880000</a>
</div> </div>
</div> </div>
@@ -194,7 +113,6 @@
</div> </div>
</footer> </footer>
<script>document.write('<scr'+'ipt src="config.js?v='+Date.now()+'"><\/scr'+'ipt>')</script>
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
</body> </body>
</html> </html>
+35 -9
View File
@@ -2,18 +2,22 @@
export const translations = { export const translations = {
de: { de: {
navCars: "Fahrzeuge", navCars: "Fahrzeuge",
navWhy: "Warum wir",
navReviews: "Stimmen", navReviews: "Stimmen",
navBook: "Buchen", navBook: "Buchen",
bookNow: "Jetzt buchen", bookNow: "Jetzt buchen",
viewFleet: "Flotte ansehen",
heroEyebrow: "MC Cars · Sportwagenvermietung", heroEyebrow: "MC Cars · Sportwagenvermietung",
heroTitle: "Fahren auf höchstem Niveau.", heroTitle: "Fahren auf höchstem Niveau.",
heroLead: "Der Ferrari in der Steiermark. Faire Kaution, transparent, sofort startklar.", heroLead: "Premium-Sportwagen und Luxusklasse in der Steiermark. Faire Kaution, transparent, sofort startklar.",
statDeposit: "Faire Kaution", statDeposit: "Faire Kaution",
statSupport: "Support",
statCars: "Fahrzeuge", statCars: "Fahrzeuge",
statCar: "Fahrzeug", statCar: "Fahrzeug",
fleetEyebrow: "Unsere Flotte",
fleetTitle: "Handverlesen. Gepflegt. Startklar.", fleetTitle: "Handverlesen. Gepflegt. Startklar.",
fleetSub: "Filtern Sie nach Marke und Preis. Klicken Sie für Details oder buchen Sie direkt.", fleetSub: "Filtern Sie nach Marke und Preis. Klicken Sie für Details oder buchen Sie direkt.",
filterBrand: "Marke", filterBrand: "Marke",
@@ -33,6 +37,15 @@ export const translations = {
from: "ab", from: "ab",
noMatches: "Keine Fahrzeuge gefunden.", noMatches: "Keine Fahrzeuge gefunden.",
whyEyebrow: "Warum MC Cars",
whyTitle: "Keine Kompromisse zwischen Sicherheit und Fahrspaß.",
whyInsurance: "Versicherungsschutz",
whyInsuranceText: "Vollkasko mit klarem Selbstbehalt. Transparente Kosten auf jedem Kilometer.",
whyFleet: "Premium Flotte",
whyFleetText: "Handverlesene Performance-Modelle, professionell gewartet und sofort startklar.",
whyDeposit: "Faire Kaution",
whyDepositText: "Zwei Kautionsarten: Bar oder PayPal-Kaution. Bei PayPal senden wir einen Deposit-Link. Bar wird aktuell persönlich bei der Fahrzeugübergabe abgewickelt.",
reviewsEyebrow: "Kundenmeinungen", reviewsEyebrow: "Kundenmeinungen",
reviewsTitle: "Erlebnisse, die bleiben.", reviewsTitle: "Erlebnisse, die bleiben.",
review: "Kundenmeinung", review: "Kundenmeinung",
@@ -101,7 +114,7 @@ export const translations = {
perWeekend: "Wochenende", perWeekend: "Wochenende",
weekendDef: "Sa 9:00 So 20:00", weekendDef: "Sa 9:00 So 20:00",
footerTagline: "Sportwagenvermietung in der Steiermark, Österreich.", footerTagline: "Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).",
footerLegal: "Rechtliches", footerLegal: "Rechtliches",
footerContact: "Kontakt", footerContact: "Kontakt",
footerNav: "Navigation", footerNav: "Navigation",
@@ -239,18 +252,22 @@ export const translations = {
}, },
en: { en: {
navCars: "Fleet", navCars: "Fleet",
navWhy: "Why us",
navReviews: "Reviews", navReviews: "Reviews",
navBook: "Book", navBook: "Book",
bookNow: "Book now", bookNow: "Book now",
viewFleet: "View fleet",
heroEyebrow: "MC Cars · Sports car rental", heroEyebrow: "MC Cars · Sports car rental",
heroTitle: "Drive at the highest level.", heroTitle: "Drive at the highest level.",
heroLead: "The Ferrari in Styria. Fair deposit, full transparency, ready to launch.", heroLead: "Premium sports and luxury cars in Styria. Fair deposit, full transparency, ready to launch.",
statDeposit: "Fair Deposit", statDeposit: "Fair Deposit",
statSupport: "Support",
statCars: "Vehicles", statCars: "Vehicles",
statCar: "Vehicle", statCar: "Vehicle",
fleetEyebrow: "Our Fleet",
fleetTitle: "Hand-picked. Maintained. Ready.", fleetTitle: "Hand-picked. Maintained. Ready.",
fleetSub: "Filter by brand or price. Click for details or book directly.", fleetSub: "Filter by brand or price. Click for details or book directly.",
filterBrand: "Brand", filterBrand: "Brand",
@@ -270,6 +287,15 @@ export const translations = {
from: "from", from: "from",
noMatches: "No vehicles match the filters.", noMatches: "No vehicles match the filters.",
whyEyebrow: "Why MC Cars",
whyTitle: "No compromises between safety and driving joy.",
whyInsurance: "Insurance",
whyInsuranceText: "Comprehensive cover with a clear deductible. Transparent costs on every kilometer.",
whyFleet: "Premium fleet",
whyFleetText: "Hand-picked performance models, professionally maintained and ready to go.",
whyDeposit: "Fair Deposit",
whyDepositText: "Two deposit options: cash or PayPal deposit. For PayPal, we send a deposit link. Cash is currently handled in person at pickup.",
reviewsEyebrow: "Testimonials", reviewsEyebrow: "Testimonials",
reviewsTitle: "Experiences that last.", reviewsTitle: "Experiences that last.",
review: "Review", review: "Review",
@@ -338,7 +364,7 @@ export const translations = {
perWeekend: "Weekend", perWeekend: "Weekend",
weekendDef: "Sat 9 AM Sun 8 PM", weekendDef: "Sat 9 AM Sun 8 PM",
footerTagline: "Sports car rental in Styria, Austria.", footerTagline: "Sports car rental in Austria. Location: Styria (TBD).",
footerLegal: "Legal", footerLegal: "Legal",
footerContact: "Contact", footerContact: "Contact",
footerNav: "Navigation", footerNav: "Navigation",
@@ -477,11 +503,11 @@ export const translations = {
}; };
export const REVIEWS = [ export const REVIEWS = [
{ quote: "Die Buchung war klar und schnell. Der Ferrari war in einem herausragenden Zustand.", author: "Martin P.", lang: "de" }, { quote: "Die Buchung war klar und schnell. Der GT3 war in einem herausragenden Zustand.", author: "Martin P.", lang: "de" },
{ quote: "Exzellenter Service und ein makellos vorbereiteter Ferrari. Unser Wochenendtrip war unvergesslich.", author: "James R.", lang: "de" }, { quote: "Exzellenter Service und makellos vorbereitete Fahrzeuge. Unser Wochenendtrip war unvergesslich.", author: "James R.", lang: "de" },
{ quote: "Hervorragende Buchungsabwicklung und tadelloser Zustand des Ferrari. Sehr zufrieden.", author: "Thomas W.", lang: "de" }, { quote: "Hervorragende Buchungsabwicklung und tadelloses Fahrzeugzustand. Sehr zufrieden.", author: "Thomas W.", lang: "de" },
{ quote: "Professionelles Team und erstklassiger Ferrari. Absolut empfehlenswert.", author: "David M.", lang: "de" }, { quote: "Professionelles Team und untadelige Aufmerksamkeit zum Detail. Sehr empfohlen.", author: "David M.", lang: "de" },
{ quote: "Booking was clear and fast. The Ferrari arrived in outstanding condition.", author: "Jonas P.", lang: "en" }, { quote: "Booking was clear and fast. The GT3 arrived in outstanding condition.", author: "Jonas P.", lang: "en" },
]; ];
export function getLang() { export function getLang() {
+16 -24
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Impressum · MC Cars (GmbH)</title> <title>Impressum · MC Cars (GmbH)</title>
<link rel="icon" type="image/svg+xml" href="/images/MC-Cars-Logo.svg" /> <link rel="icon" type="image/png" href="/images/mc-cars-logo.png" />
<link rel="apple-touch-icon" href="/images/MC-Cars-Logo.svg" /> <link rel="apple-touch-icon" href="/images/mc-cars-logo.png" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
@@ -48,12 +48,13 @@
<header class="site-header"> <header class="site-header">
<div class="shell"> <div class="shell">
<a class="logo" href="/" aria-label="MC Cars Startseite"> <a class="logo" href="/" aria-label="MC Cars Startseite">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</a> </a>
<button class="menu-toggle" aria-label="Menü"></button> <button class="menu-toggle" aria-label="Menü"></button>
<nav class="main-nav" aria-label="Hauptnavigation"> <nav class="main-nav" aria-label="Hauptnavigation">
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#stimmen" data-i18n="navReviews">Stimmen</a> <a href="/#stimmen" data-i18n="navReviews">Stimmen</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
<a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a> <a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a>
@@ -66,21 +67,11 @@
<div class="shell"> <div class="shell">
<h1>Impressum</h1> <h1>Impressum</h1>
<div style="max-width: 65ch; line-height: 1.7; color: var(--text);"> <div style="max-width: 65ch; line-height: 1.7; color: var(--text);">
<p><strong>MC Cars GmbH</strong></p> <p><strong>MC Cars (GmbH)</strong></p>
<p>Gaisfeld 1/2<br/>8564 Krottendorf-Gaisfeld</p> <p>Standort: Steiermark (TBD)</p>
<p>FN 675751 b · Landesgericht für Zivilrechtssachen Graz</p> <p>E-Mail: hello@mccars.at</p>
<p>Geschäftsführer: Christian Leski, Marco Schober</p> <p>Telefon: +43 316 880000</p>
<p>E-Mail: hello@mc-cars.at</p> <p>Firmenbuch und UID werden nachgereicht.</p>
<p>UID-Nr. wird in Kürze nachgereicht.</p>
</div>
<div style="max-width: 65ch; line-height: 1.7; color: var(--text); margin-top: 2.5rem;">
<h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 1rem; color: var(--text);">Datenschutzerklärung (Kurzfassung)</h2>
<p>Der Schutz Ihrer persönlichen Daten ist uns wichtig. Wir behandeln Ihre Daten vertraulich und entsprechend der gesetzlichen Datenschutzvorschriften, insbesondere der DSGVO und dem österreichischen Datenschutzgesetz.</p>
<p><strong>Welche Daten wir erfassen:</strong> Wir erheben nur die Daten, die für die Nutzung unserer Website und unserer Dienste unbedingt erforderlich sind. Dazu können Zugriffsdaten (Datum, Uhrzeit, besuchte Seiten), technische Daten (Browsertyp, Betriebssystem) und falls relevant von Ihnen aktiv eingegebene Daten (z.B. bei Kontakt- und Buchungsformularen) gehören.</p>
<p><strong>Wie wir Ihre Daten verwenden:</strong> Ihre Daten verwenden wir ausschließlich, um Ihnen unsere Website und die damit verbundenen Funktionen bereitzustellen, Buchungsanfragen zu bearbeiten und die Sicherheit unserer Systeme zu gewährleisten.</p>
<p><strong>Weitergabe an Dritte:</strong> Eine Weitergabe Ihrer persönlichen Daten an Dritte erfolgt grundsätzlich nicht, es sei denn, dies ist gesetzlich vorgeschrieben oder für die Erbringung unserer Dienste unerlässlich.</p>
<p><strong>Ihre Rechte:</strong> Sie haben jederzeit das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Widerspruch gegen die Verarbeitung Ihrer personenbezogenen Daten sowie das Recht auf Datenübertragbarkeit.</p>
<p>Weitere Informationen finden Sie in unserer <a href="/datenschutz" style="color: var(--accent-strong);">vollständigen Datenschutzerklärung</a>.</p>
</div> </div>
</div> </div>
</main> </main>
@@ -90,15 +81,16 @@
<div class="footer-grid"> <div class="footer-grid">
<div> <div>
<div class="logo" style="margin-bottom:0.8rem;"> <div class="logo" style="margin-bottom:0.8rem;">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</div> </div>
<p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in der Steiermark, Österreich.</p> <p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).</p>
</div> </div>
<div> <div>
<h4 data-i18n="footerNav">Navigation</h4> <h4 data-i18n="footerNav">Navigation</h4>
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
</div> </div>
@@ -112,7 +104,8 @@
<div> <div>
<h4 data-i18n="footerContact">Kontakt</h4> <h4 data-i18n="footerContact">Kontakt</h4>
<a href="mailto:hello@mc-cars.at">hello@mc-cars.at</a> <a href="mailto:hello@mccars.at">hello@mccars.at</a>
<a href="tel:+43316880000">+43 316 880000</a>
</div> </div>
</div> </div>
@@ -123,7 +116,6 @@
</div> </div>
</footer> </footer>
<script>document.write('<scr'+'ipt src="config.js?v='+Date.now()+'"><\/scr'+'ipt>')</script>
<script type="module" src="app.js"></script> <script type="module" src="app.js"></script>
</body> </body>
</html> </html>
+19 -16
View File
@@ -3,8 +3,8 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>MC Cars · Ferrari-Vermietung Steiermark</title> <title>MC Cars · Sportwagenvermietung Steiermark</title>
<meta name="description" content="MC Cars · Premium Ferrari-Vermietung in der Steiermark. Faire Kaution, transparent, sofort startklar." /> <meta name="description" content="MC Cars · Premium Sportwagen- und Luxusvermietung in der Steiermark. Faire Kaution, transparent, sofort startklar." />
<link rel="icon" type="image/svg+xml" href="/images/MC-Cars-Logo.svg" /> <link rel="icon" type="image/svg+xml" href="/images/MC-Cars-Logo.svg" />
<link rel="apple-touch-icon" href="/images/MC-Cars-Logo.svg" /> <link rel="apple-touch-icon" href="/images/MC-Cars-Logo.svg" />
<link rel="preload" as="image" href="/images/ferrari-main-car-mobile.jpg" fetchpriority="high" /> <link rel="preload" as="image" href="/images/ferrari-main-car-mobile.jpg" fetchpriority="high" />
@@ -17,7 +17,7 @@
<link rel="stylesheet" href="styles.css?v=2" /> <link rel="stylesheet" href="styles.css?v=2" />
<!-- SEO & Social Meta Tags --> <!-- SEO & Social Meta Tags -->
<meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" /> <meta name="robots" content="index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1" />
<meta name="keywords" content="Sportwagenvermietung Steiermark, Luxusauto mieten, Sportwagenverleih, Ferrari mieten Graz" /> <meta name="keywords" content="Sportwagenvermietung Steiermark, Luxusauto mieten, Sportwagenverleih, Ferraris mieten Graz, Porsche mieten Österreich" />
<meta name="theme-color" content="#1a1a1a" /> <meta name="theme-color" content="#1a1a1a" />
<meta name="language" content="German" /> <meta name="language" content="German" />
<link rel="canonical" href="https://demo.lago.dev/" /> <link rel="canonical" href="https://demo.lago.dev/" />
@@ -27,8 +27,8 @@
<!-- Open Graph Tags --> <!-- Open Graph Tags -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="MC Cars Premium Ferrari-Vermietung Steiermark" /> <meta property="og:title" content="MC Cars Premium Sportwagen & Luxusvermietung" />
<meta property="og:description" content="Fahren Sie einen Ferrari in der Steiermark. Faire Kaution, transparent, sofort startklar." /> <meta property="og:description" content="Fahren Sie Premium-Sportwagen und Luxusklasse-Fahrzeuge in der Steiermark. Faire Kaution, transparent, sofort startklar." />
<meta property="og:url" content="https://demo.lago.dev/" /> <meta property="og:url" content="https://demo.lago.dev/" />
<meta property="og:site_name" content="MC Cars" /> <meta property="og:site_name" content="MC Cars" />
<meta property="og:locale" content="de_AT" /> <meta property="og:locale" content="de_AT" />
@@ -38,8 +38,8 @@
<!-- Twitter Card Tags --> <!-- Twitter Card Tags -->
<meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="MC Cars Premium Ferrari-Vermietung Steiermark" /> <meta name="twitter:title" content="MC Cars Premium Sportwagen & Luxusvermietung" />
<meta name="twitter:description" content="Fahren Sie einen Ferrari in der Steiermark. Faire Kaution, transparent, sofort startklar." /> <meta name="twitter:description" content="Fahren Sie Premium-Sportwagen in der Steiermark. Faire Kaution, transparent, sofort startklar." />
<meta name="twitter:image" content="https://demo.lago.dev/images/mc-cars-og-image.png" /> <meta name="twitter:image" content="https://demo.lago.dev/images/mc-cars-og-image.png" />
<!-- Structured Data (JSON-LD) --> <!-- Structured Data (JSON-LD) -->
@@ -50,7 +50,7 @@
"@id": "https://demo.lago.dev/#organization", "@id": "https://demo.lago.dev/#organization",
"name": "MC Cars GmbH", "name": "MC Cars GmbH",
"alternateName": "MC Cars", "alternateName": "MC Cars",
"description": "Premium Ferrari-Vermietung in der Steiermark", "description": "Premium Sportwagen- und Luxusvermietung in der Steiermark",
"url": "https://demo.lago.dev", "url": "https://demo.lago.dev",
"logo": "https://demo.lago.dev/images/MC-Cars-Logo.svg", "logo": "https://demo.lago.dev/images/MC-Cars-Logo.svg",
"image": "https://demo.lago.dev/images/mc-cars-og-image.png", "image": "https://demo.lago.dev/images/mc-cars-og-image.png",
@@ -63,10 +63,10 @@
} }
}, },
"priceRange": "€€€", "priceRange": "€€€",
"serviceType": "Ferrari-Vermietung", "serviceType": "Sportwagenvermietung",
"sameAs": [ "sameAs": [
"https://www.facebook.com/mc-cars", "https://www.facebook.com/mccars",
"https://www.instagram.com/mc-cars" "https://www.instagram.com/mccars"
] ]
} }
</script> </script>
@@ -77,7 +77,7 @@
"name": "MC Cars GmbH", "name": "MC Cars GmbH",
"url": "https://demo.lago.dev", "url": "https://demo.lago.dev",
"logo": "https://demo.lago.dev/images/MC-Cars-Logo.svg", "logo": "https://demo.lago.dev/images/MC-Cars-Logo.svg",
"description": "Premium Ferrari-Vermietung in Steiermark, Österreich", "description": "Premium Sportwagen- und Luxusvermietung in Steiermark, Österreich",
"foundingDate": "2024", "foundingDate": "2024",
"contactPoint": { "contactPoint": {
"@type": "ContactPoint", "@type": "ContactPoint",
@@ -127,15 +127,16 @@
<div class="shell"> <div class="shell">
<p class="eyebrow" data-i18n="heroEyebrow">MC Cars · Sportwagenvermietung</p> <p class="eyebrow" data-i18n="heroEyebrow">MC Cars · Sportwagenvermietung</p>
<h1 data-i18n="heroTitle">Fahren auf höchstem Niveau.</h1> <h1 data-i18n="heroTitle">Fahren auf höchstem Niveau.</h1>
<p class="lead" data-i18n="heroLead">Der Ferrari in der Steiermark. Faire Kaution, transparent, sofort startklar.</p> <p class="lead" data-i18n="heroLead">Premium-Sportwagen und Luxusklasse in der Steiermark. Kautionsfrei, transparent, sofort startklar.</p>
<div class="hero-cta"> <div class="hero-cta">
<a class="btn" href="#buchen" data-i18n="bookNow">Jetzt buchen</a> <a class="btn" href="#buchen" data-i18n="bookNow">Jetzt buchen</a>
<a class="btn ghost" href="#fahrzeuge" data-i18n="viewFleet">Flotte ansehen</a>
</div> </div>
<div class="hero-stats"> <div class="hero-stats">
<div><strong data-i18n="statDeposit">Faire Kaution</strong><span>Fair Deposit</span></div> <div><strong data-i18n="statDeposit">Faire Kaution</strong><span>Fair Deposit</span></div>
<div><strong id="statCarsCount"></strong><span id="statCarsLabel" data-i18n="statCar">Fahrzeug</span></div> <div><strong id="statCarsCount"></strong><span id="statCarsLabel" data-i18n="statCars">Fahrzeuge</span></div>
<div><strong>24/7</strong><span data-i18n="statSupport">Support</span></div> <div><strong>24/7</strong><span data-i18n="statSupport">Support</span></div>
</div> </div>
</div> </div>
@@ -146,6 +147,7 @@
<div class="shell"> <div class="shell">
<div class="section-head"> <div class="section-head">
<div> <div>
<p class="eyebrow" data-i18n="fleetEyebrow">Unsere Flotte</p>
<h2 data-i18n="fleetTitle">Handverlesen. Gepflegt. Startklar.</h2> <h2 data-i18n="fleetTitle">Handverlesen. Gepflegt. Startklar.</h2>
<p class="sub" data-i18n="fleetSub">Filtern Sie nach Marke und Preis. Klicken Sie für Details oder buchen Sie direkt.</p> <p class="sub" data-i18n="fleetSub">Filtern Sie nach Marke und Preis. Klicken Sie für Details oder buchen Sie direkt.</p>
</div> </div>
@@ -369,7 +371,7 @@
<img src="/images/MC-Cars-Logo.svg" alt="MC Cars" class="logo-icon" style="width:2rem;height:2rem;" /> <img src="/images/MC-Cars-Logo.svg" alt="MC Cars" class="logo-icon" style="width:2rem;height:2rem;" />
<span>MC Cars</span> <span>MC Cars</span>
</div> </div>
<p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in der Steiermark, Österreich.</p> <p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).</p>
</div> </div>
<div> <div>
@@ -388,7 +390,8 @@
<div> <div>
<h4 data-i18n="footerContact">Kontakt</h4> <h4 data-i18n="footerContact">Kontakt</h4>
<a href="mailto:hello@mc-cars.at">hello@mc-cars.at</a> <a href="mailto:hello@mccars.at">hello@mccars.at</a>
<a href="tel:+43316880000">+43 316 880000</a>
</div> </div>
</div> </div>
+12 -9
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Mietbedingungen · MC Cars</title> <title>Mietbedingungen · MC Cars</title>
<link rel="icon" type="image/svg+xml" href="/images/MC-Cars-Logo.svg" /> <link rel="icon" type="image/png" href="/images/mc-cars-logo.png" />
<link rel="apple-touch-icon" href="/images/MC-Cars-Logo.svg" /> <link rel="apple-touch-icon" href="/images/mc-cars-logo.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;600;700&display=swap" rel="stylesheet" /> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@500;600;700&display=swap" rel="stylesheet" />
@@ -51,12 +51,13 @@
<header class="site-header"> <header class="site-header">
<div class="shell"> <div class="shell">
<a class="logo" href="/" aria-label="MC Cars Startseite"> <a class="logo" href="/" aria-label="MC Cars Startseite">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</a> </a>
<button class="menu-toggle" aria-label="Menü"></button> <button class="menu-toggle" aria-label="Menü"></button>
<nav class="main-nav" aria-label="Hauptnavigation"> <nav class="main-nav" aria-label="Hauptnavigation">
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#stimmen" data-i18n="navReviews">Stimmen</a> <a href="/#stimmen" data-i18n="navReviews">Stimmen</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
<a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a> <a class="btn small" href="/#buchen" data-i18n="bookNow">Jetzt buchen</a>
@@ -76,7 +77,7 @@
Die Mietbedingungen regeln die Nutzung der Mietfahrzeuge, Zahlungsbedingungen, Haftung und Versicherung. Die Mietbedingungen regeln die Nutzung der Mietfahrzeuge, Zahlungsbedingungen, Haftung und Versicherung.
</p> </p>
<p> <p>
Bitte wenden Sie sich an hello@mc-cars.at für weitere Informationen. Bitte wenden Sie sich an hello@mccars.at für weitere Informationen.
</p> </p>
</div> </div>
</div> </div>
@@ -87,15 +88,16 @@
<div class="footer-grid"> <div class="footer-grid">
<div> <div>
<div class="logo" style="margin-bottom:0.8rem;"> <div class="logo" style="margin-bottom:0.8rem;">
<img class="logo-icon" src="/images/MC-Cars-Logo.svg" alt="MC Cars Logo" onerror="this.style.display='none'" /> <img class="logo-icon" src="/images/mc-cars-logo.png" alt="MC Cars Logo" onerror="this.style.display='none'" />
<span>MC Cars</span> <span>MC Cars</span>
</div> </div>
<p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in der Steiermark, Österreich.</p> <p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).</p>
</div> </div>
<div> <div>
<h4 data-i18n="footerNav">Navigation</h4> <h4 data-i18n="footerNav">Navigation</h4>
<a href="/#fahrzeuge" data-i18n="navCars">Fahrzeuge</a> <a href="/" data-i18n="navCars">Fahrzeuge</a>
<a href="/#warum" data-i18n="navWhy">Warum wir</a>
<a href="/#buchen" data-i18n="navBook">Buchen</a> <a href="/#buchen" data-i18n="navBook">Buchen</a>
</div> </div>
@@ -109,7 +111,8 @@
<div> <div>
<h4 data-i18n="footerContact">Kontakt</h4> <h4 data-i18n="footerContact">Kontakt</h4>
<a href="mailto:hello@mc-cars.at">hello@mc-cars.at</a> <a href="mailto:hello@mccars.at">hello@mccars.at</a>
<a href="tel:+43316880000">+43 316 880000</a>
</div> </div>
</div> </div>
-306
View File
@@ -420,146 +420,6 @@ select:focus, input:focus, textarea:focus {
box-shadow: 0 4px 12px rgba(0,0,0,0.5); box-shadow: 0 4px 12px rgba(0,0,0,0.5);
} }
/* Photo carousel nav */
.vehicle-photo-nav {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
opacity: 0;
transition: opacity 0.3s ease;
pointer-events: none;
}
.vehicle-photo:hover .vehicle-photo-nav {
opacity: 1;
pointer-events: auto;
}
.vehicle-photo-prev,
.vehicle-photo-next {
background: rgba(0,0,0,0.6);
color: #fff;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.vehicle-photo-prev:hover,
.vehicle-photo-next:hover {
background: rgba(0,0,0,0.8);
}
.vehicle-photo-dots {
position: absolute;
bottom: 0.6rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
}
.vehicle-photo-dots span {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.4);
transition: background 0.2s ease, transform 0.2s ease;
}
.vehicle-photo-dots span.active {
background: #fff;
transform: scale(1.3);
}
/* Dialog gallery */
.dialog-gallery {
position: relative;
width: 100%;
aspect-ratio: 16/10;
background: #0e1015;
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 1.2rem;
}
.dialog-gallery-main {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.dialog-gallery-nav {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
pointer-events: none;
}
.dialog-gallery-prev,
.dialog-gallery-next {
background: rgba(0,0,0,0.6);
color: #fff;
border: none;
border-radius: 50%;
width: 36px;
height: 36px;
font-size: 1.4rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
pointer-events: auto;
}
.dialog-gallery-prev:hover,
.dialog-gallery-next:hover {
background: rgba(0,0,0,0.8);
}
.dialog-gallery-thumbs {
position: absolute;
bottom: 0.6rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
}
.dialog-gallery-thumbs button {
width: 56px;
height: 36px;
border-radius: 6px;
overflow: hidden;
border: 2px solid transparent;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s ease, border-color 0.2s ease;
padding: 0;
background: none;
}
.dialog-gallery-thumbs button.active {
border-color: #fff;
opacity: 1;
}
.dialog-gallery-thumbs button:hover {
opacity: 0.9;
}
.dialog-gallery-thumbs button img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.vehicle-body { .vehicle-body {
padding: 1.4rem; padding: 1.4rem;
display: flex; display: flex;
@@ -1135,172 +995,6 @@ table.admin-table td:last-child { white-space: nowrap; }
filter: brightness(1.1); filter: brightness(1.1);
} }
/* ---- Unified Photo Upload Zone ---- */
.admin-photo-upload-zone {
width: 100%;
min-height: 120px;
border: 2px dashed var(--line);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: border-color 0.2s ease, background 0.2s ease;
background: var(--bg-elev);
margin-bottom: 0.5rem;
position: relative;
}
.admin-photo-upload-zone:hover {
border-color: var(--accent-strong);
}
.admin-photo-upload-zone.drag-active {
border-color: var(--accent-strong);
background: rgba(245, 158, 11, 0.08);
}
.admin-photo-upload-zone input[type="file"] {
position: absolute;
inset: 0;
opacity: 0;
cursor: pointer;
}
.admin-photo-upload-content {
text-align: center;
pointer-events: none;
}
.admin-photo-upload-icon {
font-size: 2rem;
display: block;
margin-bottom: 0.3rem;
}
/* ---- Photo Gallery ---- */
.admin-photo-gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.8rem;
margin-top: 1rem;
}
.admin-photo-card {
position: relative;
border-radius: 10px;
overflow: hidden;
aspect-ratio: 16/10;
background: #1a1a1a;
border: 2px solid transparent;
transition: border-color 0.2s ease, transform 0.2s ease, opacity 0.2s ease;
cursor: grab;
}
.admin-photo-card:hover {
border-color: var(--accent-strong);
transform: scale(1.02);
}
.admin-photo-card:active {
cursor: grabbing;
}
.admin-photo-card-drag-over {
border-color: #f59e0b !important;
transform: scale(1.05);
}
.admin-photo-card img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
pointer-events: none;
}
.admin-photo-card-arrows {
position: absolute;
top: 6px;
left: 6px;
display: flex;
gap: 3px;
z-index: 2;
}
.admin-photo-arrow {
width: 26px;
height: 26px;
border-radius: 6px;
border: none;
background: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.admin-photo-arrow:hover {
background: rgba(0, 0, 0, 0.9);
}
.admin-photo-card-actions {
position: absolute;
top: 6px;
right: 6px;
display: flex;
gap: 3px;
z-index: 2;
}
.admin-photo-set-primary {
width: 26px;
height: 26px;
border-radius: 6px;
border: none;
background: rgba(245, 158, 11, 0.85);
color: #000;
font-size: 1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.admin-photo-set-primary:hover {
background: #f59e0b;
}
.admin-photo-delete {
width: 26px;
height: 26px;
border-radius: 6px;
border: none;
background: rgba(239, 68, 68, 0.85);
color: #fff;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s ease;
}
.admin-photo-delete:hover {
background: #ef4444;
}
.admin-photo-badge {
position: absolute;
bottom: 6px;
left: 6px;
background: #22c55e;
color: #fff;
border-radius: 5px;
padding: 2px 8px;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.05em;
z-index: 2;
}
.admin-photo-drag-handle {
position: absolute;
bottom: 6px;
right: 6px;
color: rgba(255, 255, 255, 0.5);
font-size: 1.1rem;
z-index: 2;
pointer-events: none;
}
/* ---------------- Forms / Toggle Switch ---------------- */ /* ---------------- Forms / Toggle Switch ---------------- */
.toggle-switch { .toggle-switch {
position: relative; position: relative;
-21
View File
@@ -1,21 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'list',
use: {
baseURL: process.env.APP_URL || 'http://localhost:55580',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
});
-91
View File
@@ -1,91 +0,0 @@
-- 17-vehicle-photos.sql
-- Idempotent migration: add vehicle_photos table for multiple photos per vehicle.
-- Each vehicle can have multiple photos with ordering support.
-- Create vehicle_photos table
create table if not exists public.vehicle_photos (
id uuid primary key default gen_random_uuid(),
vehicle_id uuid not null references public.vehicles(id) on delete cascade,
photo_url text not null default '',
photo_path text not null,
display_order integer not null default 0,
is_primary boolean not null default false,
created_at timestamptz not null default now()
);
create index if not exists vehicle_photos_vehicle_id_idx
on public.vehicle_photos(vehicle_id, display_order);
-- Enable RLS
alter table public.vehicle_photos enable row level security;
-- Drop existing policies to ensure idempotency
drop policy if exists "vehicle_photos_public_read" on public.vehicle_photos;
drop policy if exists "vehicle_photos_admin_read" on public.vehicle_photos;
drop policy if exists "vehicle_photos_admin_insert" on public.vehicle_photos;
drop policy if exists "vehicle_photos_admin_delete" on public.vehicle_photos;
drop policy if exists "vehicle_photos_admin_update" on public.vehicle_photos;
-- Public can read all photos
create policy "vehicle_photos_public_read"
on public.vehicle_photos for select
to anon using (true);
-- Authenticated (admin) full access
create policy "vehicle_photos_admin_read"
on public.vehicle_photos for select
to authenticated using (true);
create policy "vehicle_photos_admin_insert"
on public.vehicle_photos for insert
to authenticated with check (true);
create policy "vehicle_photos_admin_update"
on public.vehicle_photos for update
to authenticated using (true) with check (true);
create policy "vehicle_photos_admin_delete"
on public.vehicle_photos for delete
to authenticated using (true);
-- Grants
grant select on public.vehicle_photos to anon, authenticated;
grant insert, update, delete on public.vehicle_photos to authenticated;
grant all on public.vehicle_photos to service_role;
-- Migrate existing vehicle photo_url/photo_path to vehicle_photos table
-- This ensures existing vehicles get their photo into the new table
insert into public.vehicle_photos (vehicle_id, photo_url, photo_path, display_order, is_primary)
select id, photo_url, coalesce(photo_path, 'legacy'), 0, true
from public.vehicles
where photo_url != '' and photo_path is not null
on conflict do nothing;
-- RPC: set primary photo for a vehicle (unsets others)
create or replace function public.set_primary_vehicle_photo(
p_vehicle_id uuid,
p_photo_id uuid
) returns void
language plpgsql security invoker as $$
begin
update public.vehicle_photos set is_primary = false where vehicle_id = p_vehicle_id;
update public.vehicle_photos set is_primary = true where id = p_photo_id and vehicle_id = p_vehicle_id;
end;
$$;
-- RPC: re-order photos for a vehicle
create or replace function public.reorder_vehicle_photos(
p_vehicle_id uuid,
p_photo_orders jsonb -- [{id: uuid, order: int}, ...]
) returns void
language plpgsql security invoker as $$
declare
rec jsonb;
begin
for rec in select * from jsonb_array_elements(p_photo_orders) loop
update public.vehicle_photos
set display_order = (rec->>'order')::int
where id = (rec->>'id')::uuid and vehicle_id = p_vehicle_id;
end loop;
end;
$$;
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"status": "passed", "status": "failed",
"failedTests": [] "failedTests": []
} }
-255
View File
@@ -1,255 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('Booking Flow End-to-End', () => {
const ADMIN_URL = 'http://localhost:55581';
const ADMIN_EMAIL = 'admin@mccars.local';
const ADMIN_PASSWORD = 'mc-cars-admin';
// Generate unique test data per run to avoid conflicts
const ts = Date.now();
const testEmails = [
`test-day-${ts}@playwright.test`,
`test-weekend-${ts}@playwright.test`,
`test-custom-${ts}@playwright.test`,
];
const testNames = [
'Test Testerson Day',
'Test Testerson Weekend',
'Test Testerson Custom',
];
/**
* Helper: fill out the booking form for a given mietdauer type.
* Returns nothing - the form submission is handled by the page's JS.
*/
async function submitBooking(page, type, index) {
// Scroll to booking section
await page.locator('#buchen').scrollIntoViewIfNeeded();
await page.waitForTimeout(500);
// Step 1: Select vehicle
const carSelect = page.locator('#bpfCar');
await expect(carSelect).toBeVisible({ timeout: 10000 });
// Select first available vehicle option (skip the placeholder)
const options = await carSelect.locator('option').all();
expect(options.length).toBeGreaterThan(1);
const firstVehicle = await options[1].innerText();
await carSelect.selectOption({ label: firstVehicle });
// Step 2: Select mietdauer type
const presetBtn = page.locator(`.bpf-preset[data-preset="${type}"]`);
await expect(presetBtn).toBeVisible();
await presetBtn.click();
// Step 3: Pick date(s) based on type
if (type === 'day') {
// Pick a date 7 days from now
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + 7);
const dateStr = futureDate.toISOString().split('T')[0];
const dateInput = page.locator('#bpfDayDate');
await dateInput.fill(dateStr);
} else if (type === 'weekend') {
// Pick next Saturday
const nextSaturday = new Date();
const daysUntilSaturday = (6 - nextSaturday.getDay() + 7) % 7 || 7;
nextSaturday.setDate(nextSaturday.getDate() + daysUntilSaturday);
const dateStr = nextSaturday.toISOString().split('T')[0];
const dateInput = page.locator('#bpfWeekendDate');
await dateInput.fill(dateStr);
} else if (type === 'custom') {
// Pick start date 14 days from now, end date 17 days from now (4 days = individuell)
const startDate = new Date();
startDate.setDate(startDate.getDate() + 14);
const endDate = new Date(startDate);
endDate.setDate(endDate.getDate() + 3);
const fromStr = startDate.toISOString().split('T')[0];
const toStr = endDate.toISOString().split('T')[0];
await page.locator('#bpfFrom').fill(fromStr);
await page.locator('#bpfTo').fill(toStr);
}
// Click Weiter to go to step 2
await page.locator('#bpfNext1').click();
await page.waitForTimeout(300);
// Step 2: Fill contact info
await expect(page.locator('#bpfName')).toBeVisible();
await page.locator('#bpfName').fill(testNames[index]);
await page.locator('#bpfEmail').fill(testEmails[index]);
await page.locator('#bpfPhone').fill('+43 660 1234567');
await page.locator('#bpfMessage').fill(`Test booking via playwright - ${type}`);
// Click Weiter to go to step 3
await page.locator('#bpfNext2').click();
await page.waitForTimeout(300);
// Step 3: Submit (skip file uploads - they are optional)
await expect(page.locator('#bpfSubmit')).toBeVisible();
await page.locator('#bpfSubmit').click();
// Wait for success toast
await expect(page.locator('#toast.show')).toBeVisible({ timeout: 10000 });
await page.waitForTimeout(1000);
}
test('Complete booking flow: 1 Tag, Wochenende, Individuell → 3 leads in admin → disqualify all', async ({ page, context }) => {
// ========================================
// PART 1: Submit 3 bookings on main site
// ========================================
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(2000);
// Booking 1: 1 Tag
await submitBooking(page, 'day', 0);
// Booking 2: Wochenende
await submitBooking(page, 'weekend', 1);
// Booking 3: Individuell
await submitBooking(page, 'custom', 2);
// ========================================
// PART 2: Verify 3 leads in admin panel
// ========================================
const adminCtx = await test.info().project.use.baseBrowserType?.newContext() ?? context;
const adminPage = await adminCtx.newPage();
adminPage.setDefaultTimeout(30000);
await adminPage.goto(ADMIN_URL);
await adminPage.waitForLoadState('domcontentloaded');
await adminPage.waitForTimeout(2000);
// Login
const loginForm = adminPage.locator('#loginForm');
await expect(loginForm).toBeVisible({ timeout: 10000 });
await adminPage.locator('#loginForm [name="email"]').fill(ADMIN_EMAIL);
await adminPage.locator('#loginForm [name="password"]').fill(ADMIN_PASSWORD);
await adminPage.locator('#loginForm [type="submit"]').click();
// Wait a moment for login to process
await adminPage.waitForTimeout(3000);
// Check for login error
const loginError = adminPage.locator('#loginError');
if (await loginError.isVisible()) {
const errorMsg = await loginError.textContent();
throw new Error(`Login failed: ${errorMsg}`);
}
// Check if password rotation is required (first login)
const rotateView = adminPage.locator('#rotateView');
if (await rotateView.isVisible({ timeout: 2000 })) {
// Set a new password (must be different from bootstrap)
const newPw = 'Playwright-Test-PW-2026!';
await adminPage.locator('#rotateForm [name="pw1"]').fill(newPw);
await adminPage.locator('#rotateForm [name="pw2"]').fill(newPw);
await adminPage.locator('#rotateForm [type="submit"]').click();
await adminPage.waitForTimeout(2000);
}
// Wait for admin view to load
await expect(adminPage.locator('#adminView')).toBeVisible({ timeout: 15000 });
await adminPage.waitForTimeout(2000);
// Ensure leads tab is active (it's the default)
const leadsTab = adminPage.locator('[data-tab="leads"]');
const leadsTabClass = await leadsTab.getAttribute('class');
if (!leadsTabClass?.includes('active')) {
await leadsTab.click();
await adminPage.waitForTimeout(1000);
}
// Wait for our test leads to appear by checking for their emails in the table
// We wait for at least one of our test emails to appear, then verify all 3
await adminPage.waitForFunction(
([emails]) => {
const rows = document.querySelectorAll('#leadsTable tbody tr');
let found = 0;
for (const row of rows) {
const text = row.textContent;
for (const email of emails) {
if (text.includes(email)) {
found++;
break;
}
}
}
return found >= 3;
},
testEmails,
{ timeout: 30000 }
);
await adminPage.waitForTimeout(1000);
// Find our test leads by email pattern
const allRows = adminPage.locator('#leadsTable tbody tr');
const totalRows = await allRows.count();
const testRowIndices = [];
for (let i = 0; i < totalRows; i++) {
const row = allRows.nth(i);
const rowText = await row.textContent();
if (testEmails.some(email => rowText.includes(email))) {
testRowIndices.push(i);
}
}
expect(testRowIndices.length).toBe(3);
// ========================================
// PART 3: Disqualify all 3 test leads
// ========================================
// Disqualify each lead one at a time, re-finding it after each disqualification
// since the table re-renders and indices shift.
for (const email of testEmails) {
// Find the row for this email
const rows = adminPage.locator('#leadsTable tbody tr');
const count = await rows.count();
let found = false;
for (let i = 0; i < count; i++) {
const rowText = await rows.nth(i).textContent();
if (rowText.includes(email)) {
// Click disqualify button
const disqBtn = rows.nth(i).locator('[data-disq]');
if (await disqBtn.isVisible()) {
await disqBtn.click();
await adminPage.waitForTimeout(1500);
found = true;
break;
}
}
}
expect(found).toBe(true, `Lead with email ${email} not found or could not be disqualified`);
}
// Wait for disqualifications to process
await adminPage.waitForTimeout(2000);
// Refresh page to ensure fresh data after disqualifications
await adminPage.reload();
await expect(adminPage.locator('#adminView')).toBeVisible({ timeout: 15000 });
await adminPage.waitForTimeout(3000);
// Verify our test leads are now disqualified (no longer in active view)
const remainingRows = adminPage.locator('#leadsTable tbody tr');
const remainingCount = await remainingRows.count();
let foundTestLead = false;
for (let i = 0; i < remainingCount; i++) {
const rowText = await remainingRows.nth(i).textContent();
if (testEmails.some(email => rowText.includes(email))) {
foundTestLead = true;
break;
}
}
expect(foundTestLead).toBe(false);
await adminPage.close();
});
});
-36
View File
@@ -1,36 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('Legal Pages - Warum wir removed', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
});
test('Impressum page - Warum wir nav link removed', async ({ page }) => {
await page.goto('/impressum.html');
await expect(page.getByText('Warum wir')).not.toBeVisible();
});
test('AGB page - Warum wir nav link removed', async ({ page }) => {
await page.goto('/agb.html');
await expect(page.getByText('Warum wir')).not.toBeVisible();
});
test('Datenschutz page - Warum wir nav link removed', async ({ page }) => {
await page.goto('/datenschutz.html');
await expect(page.getByText('Warum wir')).not.toBeVisible();
});
test('Mietbedingungen page - Warum wir nav link removed', async ({ page }) => {
await page.goto('/mietbedingungen.html');
await expect(page.getByText('Warum wir')).not.toBeVisible();
});
test('All legal pages - other nav links present', async ({ page }) => {
await page.goto('/impressum.html');
const nav = page.getByLabel('Hauptnavigation');
await expect(nav.getByRole('link', { name: 'Fahrzeuge' }).first()).toBeVisible();
await expect(nav.getByRole('link', { name: 'Buchen' }).first()).toBeVisible();
});
});
-109
View File
@@ -1,109 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('MC Cars - Customer Changes Verification', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
});
test('Page loads successfully', async ({ page }) => {
await expect(page).toHaveTitle(/MC Cars/);
});
test('Hero section - Flotte ansehen button removed', async ({ page }) => {
await expect(page.getByText('Flotte ansehen')).not.toBeVisible();
await expect(page.getByText('View fleet')).not.toBeVisible();
});
test('Hero section - 24/7 Support stat removed', async ({ page }) => {
await expect(page.getByText('24/7')).not.toBeVisible();
});
test('Hero section - Faire Kaution stat still visible', async ({ page }) => {
const kautionStat = page.getByText('Faire Kaution', { exact: true });
await expect(kautionStat).toBeVisible();
});
test('Hero section - Fahrzeuge stat still visible', async ({ page }) => {
const vehiclesSection = page.locator('.hero-stats');
await expect(vehiclesSection).toBeVisible();
});
test('Fleet section - Unsere Flotte eyebrow removed', async ({ page }) => {
await expect(page.getByText('Unsere Flotte')).not.toBeVisible();
await expect(page.getByText('Our Fleet')).not.toBeVisible();
});
test('Fleet section - Title still visible', async ({ page }) => {
await expect(page.getByText('Handverlesen. Gepflegt. Startklar.')).toBeVisible();
});
test('Navigation - Warum wir link removed', async ({ page }) => {
await expect(page.getByText('Warum wir')).not.toBeVisible();
await expect(page.getByText('Why us')).not.toBeVisible();
});
test('Navigation - Other links still present', async ({ page }) => {
const nav = page.getByLabel('Hauptnavigation');
await expect(nav.getByRole('link', { name: 'Fahrzeuge' }).first()).toBeVisible();
await expect(nav.getByRole('link', { name: 'Stimmen' })).toBeVisible();
await expect(nav.getByRole('link', { name: 'Buchen' }).first()).toBeVisible();
await expect(nav.getByRole('link', { name: 'Jetzt buchen' })).toBeVisible();
});
test('Reviews - Ferrari references in reviews', async ({ page }) => {
await page.locator('#stimmen').scrollIntoViewIfNeeded();
const reviewsSection = page.locator('#stimmen');
await expect(reviewsSection).toBeVisible();
});
test('Reviews - GT3 references removed', async ({ page }) => {
await expect(page.getByText('GT3')).not.toBeVisible();
});
test('Footer - correct content', async ({ page }) => {
await expect(page.getByText('Rechtliches')).toBeVisible();
await expect(page.getByText('Impressum')).toBeVisible();
await expect(page.getByText('Datenschutz')).toBeVisible();
await expect(page.getByText('hello@mc-cars.at')).toBeVisible();
});
test('Footer - Steiermark reference updated', async ({ page }) => {
await expect(page.getByText('Made in Steiermark')).toBeVisible();
});
test('Language toggle works', async ({ page }) => {
const langToggle = page.locator('.lang-toggle');
await expect(langToggle).toBeVisible();
// Switch to English
await langToggle.click();
await page.waitForTimeout(500);
await expect(langToggle).toHaveText('DE');
await expect(page.getByText('Drive at the highest level.')).toBeVisible();
// Switch back to German
await langToggle.click();
await page.waitForTimeout(500);
await expect(langToggle).toHaveText('EN');
await expect(page.getByRole('heading', { name: /Niveau/ })).toBeVisible();
});
test('Fleet section - vehicle cards visible', async ({ page }) => {
await page.locator('#fahrzeuge').scrollIntoViewIfNeeded();
const vehicleCards = page.locator('.vehicle-card');
await expect(vehicleCards.first()).toBeVisible();
});
test('Booking section visible', async ({ page }) => {
await page.locator('#buchen').scrollIntoViewIfNeeded();
await expect(page.getByRole('heading', { name: 'Jetzt buchen' })).toBeVisible();
});
test('SEO title updated', async ({ page }) => {
const title = await page.title();
expect(title).toContain('Ferrari');
});
});
-41
View File
@@ -1,41 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('Photo Gallery Feature', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await page.waitForLoadState('domcontentloaded');
await page.waitForTimeout(1000);
});
test('Vehicle cards render correctly', async ({ page }) => {
await page.locator('#fahrzeuge').scrollIntoViewIfNeeded();
const cards = page.locator('.vehicle-card');
await expect(cards.first()).toBeVisible();
});
test('Vehicle card has photo', async ({ page }) => {
await page.locator('#fahrzeuge').scrollIntoViewIfNeeded();
const firstPhoto = page.locator('.vehicle-card').first().locator('.vehicle-photo img');
await expect(firstPhoto).toBeVisible();
const src = await firstPhoto.getAttribute('src');
expect(src).not.toBeNull();
expect(src).not.toBe('');
});
test('Vehicle details dialog opens', async ({ page }) => {
await page.locator('#fahrzeuge').scrollIntoViewIfNeeded();
const detailsBtn = page.locator('[data-details]').first();
if (await detailsBtn.isVisible()) {
await detailsBtn.click();
const dialog = page.locator('#carDialog');
await expect(dialog).toBeVisible();
}
});
test('Booking wizard - vehicle selector works', async ({ page }) => {
await page.locator('#buchen').scrollIntoViewIfNeeded();
const carSelect = page.locator('#bpfCar');
await expect(carSelect).toBeVisible();
});
});