From 1820f7d766df095a1ef302d3cd63a04b010966e3 Mon Sep 17 00:00:00 2001 From: Lago Date: Sat, 18 Apr 2026 00:01:03 +0200 Subject: [PATCH] feat(admin): replace checkbox with toggle-switch slider, add i18n multilanguage --- frontend/admin.html | 92 ++++++++++++++++++++++--------------------- frontend/admin.js | 37 ++++++++++++----- frontend/i18n.js | 96 ++++++++++++++++++++++++++++++++++++++++++++- frontend/styles.css | 43 ++++++++++++++++++++ 4 files changed, 212 insertions(+), 56 deletions(-) diff --git a/frontend/admin.html b/frontend/admin.html index bc30746..f6f4952 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -61,19 +61,20 @@

MC Cars · Admin

- Website + Website + - - - + + +
- - - + + +
@@ -82,18 +83,18 @@

Leads

- - + +
- - - - - + + + + + @@ -111,11 +112,11 @@
EingangName / E-MailFahrzeugZeitraumStatusEingangName / E-MailFahrzeugZeitraumStatus
- - - - - + + + + + @@ -129,73 +130,76 @@
Erster KontaktName / E-MailTelefonQuelle (Lead)StatusErster KontaktName / E-MailTelefonQuelle (Lead)Status
- - - - + + + + diff --git a/frontend/admin.js b/frontend/admin.js index 6e6ef0d..1a81c6a 100644 --- a/frontend/admin.js +++ b/frontend/admin.js @@ -1,4 +1,5 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; +import { getLang, setLang, t, applyI18n } from "./i18n.js"; const SUPA_URL = window.MCCARS_CONFIG?.SUPABASE_URL ?? ""; const SUPA_KEY = window.MCCARS_CONFIG?.SUPABASE_ANON_KEY || ""; @@ -12,6 +13,7 @@ const supabase = createClient(SUPA_URL, SUPA_KEY, { // ----- DOM ----- const loginView = document.querySelector("#loginView"); const adminView = document.querySelector("#adminView"); +const langToggle = document.querySelector(".lang-toggle"); const rotateView = document.querySelector("#rotateView"); const loginForm = document.querySelector("#loginForm"); const loginError = document.querySelector("#loginError"); @@ -59,6 +61,7 @@ const state = { // AUTH FLOW // ========================================================================= async function bootstrap() { + applyI18n(); const { data: { session } } = await supabase.auth.getSession(); if (session) { // Always fetch fresh user from server so metadata (must_change_password) is current. @@ -204,8 +207,8 @@ function renderVehicles() { `; tableBody.appendChild(tr); } @@ -352,12 +355,12 @@ function renderLeads() { `; leadsTableBody.appendChild(tr); @@ -385,9 +388,9 @@ function openLead(id) {
${l.is_active ? ` - - - ` : ``} + + + ` : ``}
`; leadDialog.showModal(); const note = () => document.querySelector("#leadNote").value; @@ -448,7 +451,7 @@ function renderCustomers() { `; customersTableBody.appendChild(tr); @@ -493,4 +496,18 @@ function fmtDate(iso) { return d.toLocaleString("de-AT", { dateStyle: "short", timeStyle: "short" }); } +if (langToggle) { + langToggle.addEventListener("click", () => { + const current = getLang(); + setLang(current === "de" ? "en" : "de"); + langToggle.textContent = getLang() === "de" ? "EN" : "DE"; + applyI18n(); + // Re-render JS injected text correctly + if (state.vehicles) renderVehicles(); + if (state.leads) renderLeads(); + if (state.customers) renderCustomers(); + }); + langToggle.textContent = getLang() === "de" ? "EN" : "DE"; +} + bootstrap(); diff --git a/frontend/i18n.js b/frontend/i18n.js index afa159d..e0d8687 100644 --- a/frontend/i18n.js +++ b/frontend/i18n.js @@ -69,11 +69,57 @@ export const translations = { footerNav: "Navigation", imprint: "Impressum", privacy: "Datenschutz", - terms: "Mietbedingungen", + footerTerms: "Mietbedingungen", copyright: "Alle Rechte vorbehalten.", close: "Schliessen", editVehicle: "Fahrzeug bearbeiten", + + adminNavWebsite: "Website", + adminChangePw: "Passwort aendern", + adminLogout: "Logout", + adminLeads: "Leads", + adminCustomers: "Kunden", + adminVehicles: "Fahrzeuge", + adminNewVehicle: "Neues Fahrzeug", + adminAllVehicles: "Alle Fahrzeuge", + adminPhotoUpload: "Foto hochladen (JPG/PNG/WebP, max 50 MB)", + adminPhotoUrl: "Foto-URL (wird automatisch gesetzt nach Upload)", + adminBrand: "Marke", + adminModel: "Modell", + adminPower: "PS", + adminSpeed: "Top-Speed km/h", + adminAccel: "0-100", + adminSeats: "Sitze", + adminPrice: "Preis / Tag (€)", + adminSort: "Reihenfolge", + adminLocation: "Standort", + adminDescDe: "Beschreibung (Deutsch)", + adminDescEn: "Description (English)", + adminActiveVisible: "Aktiv / auf Website sichtbar", + adminSave: "Speichern", + adminReset: "Neu", + adminPhoto: "Foto", + adminBrandTable: "Marke / Modell", + adminPriceTable: "€ / Tag", + adminActive: "Aktiv", + adminDel: "Löschen", + adminQualify: "Qualifizieren", + adminReject: "Ablehnen", + adminReopen: "Wieder öffnen", + adminDetails: "Details", + adminSetInactive: "Inaktiv setzen", + adminSetActive: "Aktiv setzen", + adminActiveLeads: "Aktive Leads", + adminClosedLeads: "Abgeschlossen", + adminSourceLead: "Quelle (Lead)", + adminFirstContact: "Erster Kontakt", + adminNameEmail: "Name / E-Mail", + adminPhone: "Telefon", + adminStatus: "Status", + adminReceived: "Eingang", + adminVehicleTab: "Fahrzeug", + adminPeriod: "Zeitraum", }, en: { navCars: "Fleet", @@ -144,11 +190,57 @@ export const translations = { footerNav: "Navigation", imprint: "Imprint", privacy: "Privacy", - terms: "Rental conditions", + footerTerms: "Rental conditions", copyright: "All rights reserved.", close: "Close", editVehicle: "Edit vehicle", + + adminNavWebsite: "Website", + adminChangePw: "Change password", + adminLogout: "Logout", + adminLeads: "Leads", + adminCustomers: "Customers", + adminVehicles: "Vehicles", + adminNewVehicle: "New vehicle", + adminAllVehicles: "All vehicles", + adminPhotoUpload: "Upload photo (JPG/PNG/WebP, max 50 MB)", + adminPhotoUrl: "Photo URL (auto-set after upload)", + adminBrand: "Brand", + adminModel: "Model", + adminPower: "HP", + adminSpeed: "Top speed km/h", + adminAccel: "0-62", + adminSeats: "Seats", + adminPrice: "Price / day (€)", + adminSort: "Sort order", + adminLocation: "Location", + adminDescDe: "Description (German)", + adminDescEn: "Description (English)", + adminActiveVisible: "Active / visible on website", + adminSave: "Save", + adminReset: "New", + adminPhoto: "Photo", + adminBrandTable: "Brand / Model", + adminPriceTable: "€ / day", + adminActive: "Active", + adminDel: "Delete", + adminQualify: "Qualify", + adminReject: "Reject", + adminReopen: "Reopen", + adminDetails: "Details", + adminSetInactive: "Set inactive", + adminSetActive: "Set active", + adminActiveLeads: "Active leads", + adminClosedLeads: "Closed", + adminSourceLead: "Source (Lead)", + adminFirstContact: "First contact", + adminNameEmail: "Name / Email", + adminPhone: "Phone", + adminStatus: "Status", + adminReceived: "Received", + adminVehicleTab: "Vehicle", + adminPeriod: "Period", }, }; diff --git a/frontend/styles.css b/frontend/styles.css index edd6559..5ce9ad0 100644 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -604,6 +604,49 @@ table.admin-table tbody tr:hover { filter: brightness(1.1); } +/* ---------------- Forms / Toggle Switch ---------------- */ +.toggle-switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + flex-shrink: 0; +} +.toggle-switch input { + opacity: 0; + width: 0; + height: 0; +} +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; left: 0; right: 0; bottom: 0; + background-color: var(--line); + transition: background-color 0.3s ease; + border-radius: 24px; +} +.toggle-slider:before { + position: absolute; + content: ""; + height: 16px; + width: 16px; + left: 4px; + bottom: 4px; + background-color: var(--text); + transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); + border-radius: 50%; +} +input:checked + .toggle-slider { + background-color: var(--accent); +} +input:focus + .toggle-slider { + box-shadow: 0 0 0 2px var(--bg-card), 0 0 0 4px var(--accent); +} +input:checked + .toggle-slider:before { + transform: translateX(20px); + background-color: #111; +} + /* Admin tabs */ .admin-tabs { display: flex; gap: 0.4rem;
FotoMarke / Modell€ / TagAktivFotoMarke / Modell€ / TagAktiv
€ ${v.daily_price_eur} ${v.is_active ? "✅" : "—"} - - + + ${esc(l.date_from || "—")} → ${esc(l.date_to || "—")} ${esc(l.status)} - + ${wantActive ? ` - - + + ` : ` - + `} ${esc(c.status)}