Files

247 lines
10 KiB
JavaScript

import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4";
import { translations, REVIEWS, getLang, setLang, t, applyI18n } from "./i18n.js";
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,
};
// ---------------- 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 bookingCar = document.querySelector("#bookingCar");
const bookingForm = document.querySelector("#bookingForm");
const bookingFeedback = document.querySelector("#bookingFeedback");
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");
document.querySelector("#year").textContent = new Date().getFullYear();
// ---------------- 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 = `<p style="color:var(--danger);">Unable to load vehicles: ${error.message}</p>`;
return;
}
state.vehicles = data || [];
statCarsCount.textContent = state.vehicles.length;
const brands = [...new Set(state.vehicles.map(v => v.brand))].sort();
brandFilter.innerHTML = `<option value="all">${t("all")}</option>` +
brands.map(b => `<option value="${b}">${b}</option>`).join("");
bookingCar.innerHTML = state.vehicles
.map(v => `<option value="${v.id}">${v.brand} ${v.model}</option>`)
.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 card = document.createElement("article");
card.className = "vehicle-card";
card.innerHTML = `
<div class="vehicle-photo" role="img" aria-label="${escapeAttr(v.brand)} ${escapeAttr(v.model)}" style="background-image:url('${escapeAttr(v.photo_url)}');">
<span class="badge" aria-hidden="true">${escapeHtml(v.brand)}</span>
</div>
<div class="vehicle-body">
<p class="model-brand" aria-hidden="true">${escapeHtml(v.brand)}</p>
<h3>${escapeHtml(v.model)}</h3>
<div class="spec-row">
<div><strong>${v.power_hp}</strong><span>${t("hp")}</span></div>
<div><strong>${v.top_speed_kmh}</strong><span>${t("kmh")}</span></div>
<div><strong>${escapeHtml(v.acceleration)}</strong><span>${t("accel")}</span></div>
</div>
<div class="vehicle-footer">
<div class="vehicle-price">€ ${v.daily_price_eur}<span> / ${t("perDay")}</span></div>
<div style="display:flex;gap:0.4rem;">
<button class="btn ghost small" data-details="${v.id}" aria-label="${t("details")} ${escapeAttr(v.brand)} ${escapeAttr(v.model)}">${t("details")}</button>
<button class="btn small" data-book="${v.id}" aria-label="${t("book")} ${escapeAttr(v.brand)} ${escapeAttr(v.model)}">${t("book")}</button>
</div>
</div>
</div>
`;
grid.appendChild(card);
}
grid.querySelectorAll("[data-details]").forEach(b => {
b.addEventListener("click", () => openDetails(b.dataset.details));
});
grid.querySelectorAll("[data-book]").forEach(b => {
b.addEventListener("click", () => {
bookingCar.value = b.dataset.book;
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" });
});
});
}
function openDetails(id) {
const v = state.vehicles.find(x => x.id === id);
if (!v) return;
const lang = getLang();
const desc = lang === "en" ? v.description_en : v.description_de;
dialogTitle.textContent = `${v.brand} ${v.model}`;
dialogBody.innerHTML = `
<img src="${escapeAttr(v.photo_url)}" alt="${escapeAttr(v.brand + ' ' + v.model)}" />
<p>${escapeHtml(desc || "")}</p>
<div class="spec-row" style="margin:1rem 0;">
<div><strong>${v.power_hp}</strong><span>${t("hp")}</span></div>
<div><strong>${v.top_speed_kmh}</strong><span>${t("kmh")}</span></div>
<div><strong>${escapeHtml(v.acceleration)}</strong><span>${t("accel")}</span></div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:1rem;">
<div class="vehicle-price">€ ${v.daily_price_eur}<span> / ${t("perDay")}</span></div>
<button class="btn" id="dialogBook">${t("bookNow")}</button>
</div>
`;
dialog.showModal();
document.querySelector("#dialogBook").addEventListener("click", () => {
dialog.close();
bookingCar.value = v.id;
document.querySelector("#buchen").scrollIntoView({ behavior: "smooth" });
});
}
// ---------------- Reviews ----------------
function renderReviews() {
const list = REVIEWS[getLang()];
state.reviewIdx = state.reviewIdx % list.length;
const r = list[state.reviewIdx];
reviewStrip.innerHTML = `
<p class="review-quote">"${escapeHtml(r.quote)}"</p>
<p class="review-author">${escapeHtml(r.author)}</p>
`;
reviewDots.innerHTML = list.map((_, i) =>
`<button class="${i === state.reviewIdx ? 'active' : ''}" data-rev="${i}" aria-label="${t("review")} ${i + 1}"></button>`
).join("");
reviewDots.querySelectorAll("button").forEach(b => {
b.addEventListener("click", () => { state.reviewIdx = +b.dataset.rev; renderReviews(); });
});
}
setInterval(() => { state.reviewIdx++; renderReviews(); }, 6000);
// ---------------- Booking -> LEADS ----------------
bookingForm.addEventListener("submit", async (e) => {
e.preventDefault();
const fd = new FormData(bookingForm);
const data = Object.fromEntries(fd.entries());
if (!data.from || !data.to || new Date(data.to) <= new Date(data.from)) {
bookingFeedback.textContent = t("invalidDates");
bookingFeedback.className = "form-feedback error";
return;
}
const vehicle = state.vehicles.find(v => v.id === data.vehicle);
const payload = {
name: data.name,
email: data.email,
phone: data.phone || "",
vehicle_id: data.vehicle || null,
vehicle_label: vehicle ? `${vehicle.brand} ${vehicle.model}` : "",
date_from: data.from || null,
date_to: data.to || null,
message: data.message || "",
source: "website",
};
bookingFeedback.className = "form-feedback";
bookingFeedback.textContent = "...";
const { error } = await supabase.from("leads").insert(payload);
if (error) {
console.error(error);
bookingFeedback.className = "form-feedback error";
bookingFeedback.textContent = t("bookingFailed");
return;
}
bookingFeedback.textContent = t("bookingSuccess");
bookingForm.reset();
});
// ---------------- 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 => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;"
})[c]);
}
function escapeAttr(s) { return escapeHtml(s); }
// ---------------- Boot ----------------
langToggle.textContent = getLang() === "de" ? "EN" : "DE";
applyI18n();
renderReviews();
loadVehicles();