import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { translations, REVIEWS, getLang, setLang, t, applyI18n } from "./i18n.js?v=3"; const SUPA_URL = window.MCCARS_CONFIG?.SUPABASE_URL ?? ""; const SUPA_KEY = window.MCCARS_CONFIG?.SUPABASE_ANON_KEY || ""; export const supabase = createClient(SUPA_URL, SUPA_KEY, { auth: { persistSession: false, storageKey: "mccars.public" }, }); // ---------------- State ---------------- const state = { vehicles: [], filtered: [], brand: "all", sort: "sort_order", maxPrice: null, reviewIdx: 0, vehiclePhotosMap: new Map(), }; // ---------------- Elements ---------------- const grid = document.querySelector("#vehicleGrid"); const emptyState = document.querySelector("#emptyState"); const brandFilter = document.querySelector("#brandFilter"); const sortFilter = document.querySelector("#sortFilter"); const priceFilter = document.querySelector("#priceFilter"); const langToggle = document.querySelector(".lang-toggle"); const menuToggle = document.querySelector(".menu-toggle"); const mainNav = document.querySelector(".main-nav"); const dialog = document.querySelector("#carDialog"); const dialogTitle = document.querySelector("#dialogTitle"); const dialogBody = document.querySelector("#dialogBody"); const dialogClose = document.querySelector("#dialogClose"); const reviewStrip = document.querySelector("#reviewStrip"); const reviewDots = document.querySelector("#reviewDots"); const statCarsCount = document.querySelector("#statCarsCount"); const bookingFeedback = document.querySelector("#bookingFeedback"); // BPF elements const bpfCar = document.querySelector("#bpfCar"); const bpfFrom = document.querySelector("#bpfFrom"); const bpfTo = document.querySelector("#bpfTo"); const bpfDayDate = document.querySelector("#bpfDayDate"); const bpfWeekendDate = document.querySelector("#bpfWeekendDate"); const bpfName = document.querySelector("#bpfName"); const bpfEmail = document.querySelector("#bpfEmail"); const bpfPhone = document.querySelector("#bpfPhone"); const bpfMessage = document.querySelector("#bpfMessage"); const bpfFileId = document.querySelector("#bpfFileId"); const bpfFileIncome = document.querySelector("#bpfFileIncome"); const bpfSubmitBtn = document.querySelector("#bpfSubmit"); const bpfSidebar = document.querySelector("#bpfSidebar"); const bpfSidebarContent = document.querySelector("#bpfSidebarContent"); const bpfSidebarPlaceholder = document.querySelector(".bpf-sidebar-placeholder"); let bpfDurationMode = ""; // "day" | "weekend" | "custom" | "" let bpfSubmitting = false; function formatYmdLocal(d) { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; } function parseYmdLocal(ymd) { const [y, m, d] = (ymd || "").split("-").map(Number); if (!y || !m || !d) return null; return new Date(y, m - 1, d); } function addDaysYmd(ymd, days) { const d = parseYmdLocal(ymd); if (!d) return null; d.setDate(d.getDate() + days); return formatYmdLocal(d); } function isSaturdayYmd(ymd) { const d = parseYmdLocal(ymd); return !!d && d.getDay() === 6; } function nextSaturdayYmd(ymd) { const d = parseYmdLocal(ymd); if (!d) return null; const delta = (6 - d.getDay() + 7) % 7; d.setDate(d.getDate() + delta); return formatYmdLocal(d); } // Set min dates const today = formatYmdLocal(new Date()); [bpfFrom, bpfTo, bpfDayDate, bpfWeekendDate].forEach(el => { if (el) el.min = today; }); document.querySelector("#year").textContent = new Date().getFullYear(); // ----------------Toast Notification ---------------- function showToast(message, duration = 3000) { const toast = document.querySelector("#toast"); if (!toast) return; toast.textContent = message; toast.classList.add("show"); setTimeout(() => { toast.classList.remove("show"); }, duration); } // ---------------- Vehicles ---------------- async function loadVehicles() { const { data, error } = await supabase .from("vehicles") .select("*") .eq("is_active", true) .order("sort_order", { ascending: true }); if (error) { console.error("Failed to load vehicles", error); grid.innerHTML = `
Unable to load vehicles: ${error.message}
`; return; } state.vehicles = data || []; statCarsCount.textContent = state.vehicles.length; // Load vehicle photos if (state.vehicles.length > 0) { const ids = state.vehicles.map(v => v.id); const { data: photos } = await supabase .from("vehicle_photos") .select("*") .in("vehicle_id", ids) .order("display_order", { ascending: true }); state.vehiclePhotosMap = new Map(); if (photos) { for (const ph of photos) { if (!state.vehiclePhotosMap.has(ph.vehicle_id)) { state.vehiclePhotosMap.set(ph.vehicle_id, []); } state.vehiclePhotosMap.get(ph.vehicle_id).push(ph); } } // Also include legacy main photo if no gallery photos exist for (const v of state.vehicles) { if (!state.vehiclePhotosMap.has(v.id) && v.photo_url) { state.vehiclePhotosMap.set(v.id, [{ photo_url: v.photo_url }]); } } } const brands = [...new Set(state.vehicles.map(v => v.brand))].sort(); brandFilter.innerHTML = `` + brands.map(b => ``).join(""); // Populate BPF car selector bpfCar.innerHTML = `` + state.vehicles.map(v => ``).join(""); applyFilters(); } function applyFilters() { let rows = [...state.vehicles]; if (state.brand !== "all") rows = rows.filter(v => v.brand === state.brand); if (state.maxPrice) rows = rows.filter(v => v.daily_price_eur <= state.maxPrice); switch (state.sort) { case "priceAsc": rows.sort((a, b) => a.daily_price_eur - b.daily_price_eur); break; case "priceDesc": rows.sort((a, b) => b.daily_price_eur - a.daily_price_eur); break; case "powerDesc": rows.sort((a, b) => b.power_hp - a.power_hp); break; default: rows.sort((a, b) => a.sort_order - b.sort_order); } state.filtered = rows; renderGrid(); } function renderGrid() { grid.innerHTML = ""; emptyState.style.display = state.filtered.length ? "none" : "block"; for (const v of state.filtered) { const photos = state.vehiclePhotosMap?.get(v.id) || []; const primaryPhoto = photos.find(p => p.is_primary) || photos[0]; const photoUrl = optimizedVehiclePhotoUrl(primaryPhoto?.photo_url || v.photo_url); const photoCount = photos.length; const card = document.createElement("article"); card.className = "vehicle-card"; card.innerHTML = `${escapeHtml(desc || "")}
"${escapeHtml(r.quote)}"
`; reviewDots.innerHTML = REVIEWS.map((_, i) => `` ).join(""); reviewDots.querySelectorAll("button").forEach(b => { b.addEventListener("click", () => { state.reviewIdx = +b.dataset.rev; renderReviews(); }); }); } setInterval(() => { state.reviewIdx++; renderReviews(); }, 5000); // ---------------- BPF WIZARD ---------------- const bpfStepPanels = [ document.querySelector("#bpfStep1"), document.querySelector("#bpfStep2"), document.querySelector("#bpfStep3"), ]; const bpfStepButtons = document.querySelectorAll(".bpf-step"); let bpfCurrentStep = 1; function showBpfStep(n) { bpfCurrentStep = n; bpfStepPanels.forEach((p, i) => p.style.display = (i === n - 1) ? "block" : "none"); bpfStepButtons.forEach((b, i) => { b.classList.toggle("active", i === n - 1); b.classList.toggle("done", i < n - 1); }); } document.querySelector("#bpfNext1").addEventListener("click", () => { if (!bpfCar.value) { bpfCar.focus(); return; } // Resolve effective from/to based on duration mode const { from, to } = getBpfDates(); if (!from || !to || new Date(to) <= new Date(from)) { bookingFeedback.textContent = bpfDurationMode === "weekend" ? t("weekendSaturdayOnly") : t("invalidDates"); bookingFeedback.className = "form-feedback error"; return; } // Sync hidden from/to for downstream use bpfFrom.value = from; bpfTo.value = to; bookingFeedback.textContent = ""; showBpfStep(2); }); document.querySelector("#bpfBack2").addEventListener("click", () => showBpfStep(1)); document.querySelector("#bpfNext2").addEventListener("click", () => { if (!bpfName.value || !bpfEmail.value) { bpfEmail.focus(); return; } showBpfStep(3); }); document.querySelector("#bpfBack3").addEventListener("click", () => showBpfStep(2)); // File upload display bpfFileId.addEventListener("change", () => { document.querySelector("#bpfFileIdName").textContent = bpfFileId.files[0]?.name || ""; }); bpfFileIncome.addEventListener("change", () => { document.querySelector("#bpfFileIncomeName").textContent = bpfFileIncome.files[0]?.name || ""; }); // ---------------- Duration Presets ---------------- function setDurationMode(mode) { bpfDurationMode = mode || ""; document.querySelectorAll(".bpf-preset").forEach(b => b.classList.toggle("active", b.dataset.preset === mode)); document.querySelector("#bpfDateDay").style.display = mode === "day" ? "block" : "none"; document.querySelector("#bpfDateWeekend").style.display = mode === "weekend" ? "block" : "none"; document.querySelector("#bpfDateCustom").style.display = mode === "custom" ? "block" : "none"; updateSidebar(); } // Fresh page load: no duration selected, so no date inputs are visible. setDurationMode(""); document.querySelectorAll(".bpf-preset").forEach(btn => { btn.addEventListener("click", () => setDurationMode(btn.dataset.preset)); }); // Restrict weekend picker to Saturdays only function enforceWeekendSaturday() { if (!bpfWeekendDate.value) return; if (!isSaturdayYmd(bpfWeekendDate.value)) { bpfWeekendDate.value = ""; bookingFeedback.className = "form-feedback error"; bookingFeedback.textContent = t("weekendSaturdayOnly"); bpfWeekendDate.focus(); updateSidebar(); return; } bookingFeedback.textContent = ""; updateSidebar(); } bpfWeekendDate.addEventListener("input", enforceWeekendSaturday); bpfWeekendDate.addEventListener("change", enforceWeekendSaturday); bpfDayDate.addEventListener("change", () => { updateSidebar(); }); function getBpfDates() { if (bpfDurationMode === "day") { const d = bpfDayDate.value; if (!d) return { from: null, to: null }; return { from: d, to: addDaysYmd(d, 1) }; } if (bpfDurationMode === "weekend") { const sat = bpfWeekendDate.value; if (!sat) return { from: null, to: null }; if (!isSaturdayYmd(sat)) return { from: null, to: null }; return { from: sat, to: addDaysYmd(sat, 2) }; } // custom return { from: bpfFrom.value, to: bpfTo.value }; } // Pricing calculation function calcWeekendDays(from, to) { // Count Saturday and Sunday days in a date interval let count = 0; const start = parseYmdLocal(from); const end = parseYmdLocal(to); if (!start || !end) return 0; const cur = new Date(start); while (cur < end) { const day = cur.getDay(); // 0=Sun, 6=Sat if (day === 6 || day === 0) count++; cur.setDate(cur.getDate() + 1); } return count; } async function updateSidebar() { const v = state.vehicles.find(x => x.id === bpfCar.value); const { from, to } = getBpfDates(); if (!v || !from || !to) { bpfSidebarContent.style.display = "none"; bpfSidebarPlaceholder.style.display = "block"; return; } const fromD = parseYmdLocal(from); const toD = parseYmdLocal(to); if (!fromD || !toD) return; if (toD <= fromD) return; // Fetch price from backend RPC const { data: price, error } = await supabase.rpc("calculate_price", { p_vehicle_id: v.id, p_date_from: from, p_date_to: to, }); if (error || !price) { console.error("calculate_price error:", error, "data:", price); return; } const totalDays = price.total_days; const weekdays = price.weekday_count; const weekendDays = price.weekend_day_count; const weekdayCost = price.daily_subtotal; const weekendCost = price.weekend_subtotal; const subtotal = price.subtotal_eur; const vat = price.vat_eur; const total = price.total_eur; const deposit = price.deposit_eur; const includedKmPerDay = price.included_km_per_day || 150; const includedKm = totalDays * includedKmPerDay; const sidebarPhotos = state.vehiclePhotosMap?.get(v.id) || []; const photoUrl = optimizedVehiclePhotoUrl((sidebarPhotos.find(p => p.is_primary) || sidebarPhotos[0] || v)?.photo_url || v.photo_url); if (totalDays > 2) { // Individuell mode: show info banner instead of pricing bpfSidebarPlaceholder.style.display = "none"; bpfSidebarContent.style.display = "block"; bpfSidebarContent.innerHTML = `${escapeHtml(v.brand)} ${escapeHtml(v.model)}
${v.power_hp} ${t("hp")} • ${v.top_speed_kmh} ${t("kmh")} • ${escapeHtml(v.acceleration)}
`; } else { bpfSidebarPlaceholder.style.display = "none"; bpfSidebarContent.style.display = "block"; bpfSidebarContent.innerHTML = `${escapeHtml(v.brand)} ${escapeHtml(v.model)}
${v.power_hp} ${t("hp")} • ${v.top_speed_kmh} ${t("kmh")} • ${escapeHtml(v.acceleration)}
`; } } bpfCar.addEventListener("change", updateSidebar); bpfFrom.addEventListener("change", updateSidebar); bpfTo.addEventListener("change", updateSidebar); // Submit BPF document.querySelector("#bpfSubmit").addEventListener("click", async () => { if (bpfSubmitting) return; bpfSubmitting = true; if (bpfSubmitBtn) bpfSubmitBtn.disabled = true; bookingFeedback.className = "form-feedback"; bookingFeedback.textContent = "..."; const vehicle = state.vehicles.find(v => v.id === bpfCar.value); const { from, to } = getBpfDates(); const payload = { p_name: bpfName.value, p_email: bpfEmail.value, p_phone: bpfPhone.value || "", p_vehicle_id: bpfCar.value || null, p_vehicle_label: vehicle ? `${vehicle.brand} ${vehicle.model}` : "", p_date_from: from || null, p_date_to: to || null, p_message: bpfMessage.value || "", p_source: "website", }; // Create lead via RPC (returns inserted id without anon SELECT privileges) const { data: leadId, error } = await supabase.rpc("create_lead", payload); if (error) { console.error(error); bookingFeedback.className = "form-feedback error"; bookingFeedback.textContent = t("bookingFailed"); bpfSubmitting = false; if (bpfSubmitBtn) bpfSubmitBtn.disabled = false; return; } // Upload files const uploads = []; if (bpfFileId.files[0]) { uploads.push(uploadDoc(leadId, bpfFileId.files[0], "id_document")); } if (bpfFileIncome.files[0]) { uploads.push(uploadDoc(leadId, bpfFileIncome.files[0], "income_proof")); } await Promise.all(uploads); bookingFeedback.className = "form-feedback"; bookingFeedback.textContent = ""; showToast(t("bookingSuccess"), 4000); showBpfStep(1); bpfCar.value = ""; bpfFrom.value = ""; bpfTo.value = ""; bpfDayDate.value = ""; bpfWeekendDate.value = ""; bpfName.value = ""; bpfEmail.value = ""; bpfPhone.value = ""; bpfMessage.value = ""; document.querySelector("#bpfFileIdName").textContent = ""; document.querySelector("#bpfFileIncomeName").textContent = ""; setDurationMode(""); updateSidebar(); bpfSubmitting = false; if (bpfSubmitBtn) bpfSubmitBtn.disabled = false; }); async function uploadDoc(leadId, file, kind) { try { const ext = (file.name.split(".").pop() || "bin").toLowerCase(); const path = `${leadId}/${kind}.${ext}`; const { error: upErr } = await supabase.storage .from("customer-documents") .upload(path, file, { contentType: file.type }); if (upErr) { console.error("Upload failed:", upErr); return; } await supabase.from("lead_attachments").insert({ lead_id: leadId, bucket: "customer-documents", file_path: path, file_name: file.name, mime_type: file.type, kind: kind, }); } catch (e) { console.error("Doc upload error:", e); } } // ---------------- Events ---------------- brandFilter.addEventListener("change", e => { state.brand = e.target.value; applyFilters(); }); sortFilter.addEventListener("change", e => { state.sort = e.target.value; applyFilters(); }); priceFilter.addEventListener("input", e => { state.maxPrice = e.target.value ? +e.target.value : null; applyFilters(); }); dialogClose.addEventListener("click", () => dialog.close()); menuToggle.addEventListener("click", () => mainNav.classList.toggle("open")); mainNav.addEventListener("click", e => { if (e.target.tagName === "A") mainNav.classList.remove("open"); }); langToggle.addEventListener("click", () => { const next = getLang() === "de" ? "en" : "de"; setLang(next); langToggle.textContent = next === "de" ? "EN" : "DE"; applyI18n(); renderReviews(); applyFilters(); }); // ---------------- Helpers ---------------- function escapeHtml(s) { return String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]); } function escapeAttr(s) { return escapeHtml(s); } function optimizedVehiclePhotoUrl(url) { const raw = String(url ?? ""); if (!raw) return raw; return raw.replace("/images/ferrari-main-car.png", "/images/ferrari-main-car-mobile.jpg"); } // ---------------- Boot ---------------- langToggle.textContent = getLang() === "de" ? "EN" : "DE"; applyI18n(); renderReviews(); loadVehicles(); // Load hero image from site_settings (async () => { const { data } = await supabase.from("site_settings").select("value").eq("key", "hero_image_url").single(); if (data && data.value) { const heroUrl = data.value.includes("/images/ferrari-main-car.png") ? "/images/ferrari-main-car-mobile.jpg" : data.value; document.querySelector(".hero").style.setProperty("--hero-bg", `url('${heroUrl}')`); } })();