feat(admin): replace checkbox with toggle-switch slider, add i18n multilanguage

This commit is contained in:
Lago
2026-04-18 00:01:03 +02:00
parent c5fa51ce63
commit 1820f7d766
4 changed files with 212 additions and 56 deletions
+48 -44
View File
@@ -61,19 +61,20 @@
<div class="admin-bar"> <div class="admin-bar">
<h1>MC Cars · Admin</h1> <h1>MC Cars · Admin</h1>
<div style="display:flex;gap:0.6rem;align-items:center;flex-wrap:wrap;"> <div style="display:flex;gap:0.6rem;align-items:center;flex-wrap:wrap;">
<a href="index.html" class="btn ghost small">Website</a> <a href="index.html" class="btn ghost small" data-i18n="adminNavWebsite">Website</a>
<button class="lang-toggle" type="button" aria-label="Sprache wechseln" style="margin-left:auto;">EN</button>
<span id="adminWho" style="color:var(--muted);font-size:0.85rem;"></span> <span id="adminWho" style="color:var(--muted);font-size:0.85rem;margin-left:1rem;"></span>
<button id="changePwBtn" class="btn ghost small">Passwort aendern</button> <button id="changePwBtn" class="btn ghost small" data-i18n="adminChangePw">Passwort aendern</button>
<button id="logoutBtn" class="btn small">Logout</button> <button id="logoutBtn" class="btn small" data-i18n="adminLogout">Logout</button>
</div> </div>
</div> </div>
<!-- Tabs --> <!-- Tabs -->
<div class="admin-tabs" role="tablist"> <div class="admin-tabs" role="tablist">
<button class="tab active" data-tab="leads" role="tab">Leads <span id="leadsBadge" class="tab-badge">0</span></button> <button class="tab active" data-tab="leads" role="tab"><span data-i18n="adminLeads">Leads</span> <span id="leadsBadge" class="tab-badge">0</span></button>
<button class="tab" data-tab="customers" role="tab">Kunden <span id="customersBadge" class="tab-badge">0</span></button> <button class="tab" data-tab="customers" role="tab"><span data-i18n="adminCustomers">Kunden</span> <span id="customersBadge" class="tab-badge">0</span></button>
<button class="tab" data-tab="vehicles" role="tab">Fahrzeuge</button> <button class="tab" data-tab="vehicles" role="tab" data-i18n="adminVehicles">Fahrzeuge</button>
</div> </div>
<!-- LEADS --> <!-- LEADS -->
@@ -82,18 +83,18 @@
<div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;"> <div style="display:flex;justify-content:space-between;align-items:center;gap:1rem;flex-wrap:wrap;margin-bottom:1rem;">
<h2 style="margin:0;">Leads</h2> <h2 style="margin:0;">Leads</h2>
<div class="sub-tabs" role="tablist"> <div class="sub-tabs" role="tablist">
<button class="sub-tab active" data-lview="active">Aktive Leads</button> <button class="sub-tab active" data-lview="active" data-i18n="adminActiveLeads">Aktive Leads</button>
<button class="sub-tab" data-lview="inactive">Abgeschlossen</button> <button class="sub-tab" data-lview="inactive" data-i18n="adminClosedLeads">Abgeschlossen</button>
</div> </div>
</div> </div>
<table class="admin-table" id="leadsTable"> <table class="admin-table" id="leadsTable">
<thead> <thead>
<tr> <tr>
<th>Eingang</th> <th data-i18n="adminReceived">Eingang</th>
<th>Name / E-Mail</th> <th data-i18n="adminNameEmail">Name / E-Mail</th>
<th>Fahrzeug</th> <th data-i18n="adminVehicleTab">Fahrzeug</th>
<th>Zeitraum</th> <th data-i18n="adminPeriod">Zeitraum</th>
<th>Status</th> <th data-i18n="adminStatus">Status</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -111,11 +112,11 @@
<table class="admin-table" id="customersTable"> <table class="admin-table" id="customersTable">
<thead> <thead>
<tr> <tr>
<th>Erster Kontakt</th> <th data-i18n="adminFirstContact">Erster Kontakt</th>
<th>Name / E-Mail</th> <th data-i18n="adminNameEmail">Name / E-Mail</th>
<th>Telefon</th> <th data-i18n="adminPhone">Telefon</th>
<th>Quelle (Lead)</th> <th data-i18n="adminSourceLead">Quelle (Lead)</th>
<th>Status</th> <th data-i18n="adminStatus">Status</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@@ -129,73 +130,76 @@
<div class="tab-panel" id="tab-vehicles" style="display:none;"> <div class="tab-panel" id="tab-vehicles" style="display:none;">
<div class="admin-grid"> <div class="admin-grid">
<div class="panel"> <div class="panel">
<h2 id="formTitle">Neues Fahrzeug</h2> <h2 id="formTitle" data-i18n="adminNewVehicle">Neues Fahrzeug</h2>
<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-preview" id="photoPreview"></div> <div class="admin-photo-preview" id="photoPreview"></div>
<label> <label>
<span>Foto hochladen (JPG/PNG/WebP, max 50 MB)</span> <span data-i18n="adminPhotoUpload">Foto hochladen (JPG/PNG/WebP, max 50 MB)</span>
<input type="file" id="photoInput" accept="image/*" /> <input type="file" id="photoInput" accept="image/*" />
</label> </label>
<label> <label>
<span>Foto-URL (wird automatisch gesetzt nach Upload)</span> <span data-i18n="adminPhotoUrl">Foto-URL (wird automatisch gesetzt nach Upload)</span>
<input type="url" name="photo_url" placeholder="https://..." /> <input type="url" name="photo_url" placeholder="https://..." />
</label> </label>
<div class="row2"> <div class="row2">
<label><span>Marke</span><input name="brand" required /></label> <label><span data-i18n="adminBrand">Marke</span><input name="brand" required /></label>
<label><span>Modell</span><input name="model" required /></label> <label><span data-i18n="adminModel">Modell</span><input name="model" required /></label>
</div> </div>
<div class="row3"> <div class="row3">
<label><span>PS</span><input type="number" name="power_hp" min="0" /></label> <label><span data-i18n="adminPower">PS</span><input type="number" name="power_hp" min="0" /></label>
<label><span>Top-Speed km/h</span><input type="number" name="top_speed_kmh" min="0" /></label> <label><span data-i18n="adminSpeed">Top-Speed km/h</span><input type="number" name="top_speed_kmh" min="0" /></label>
<label><span>0-100</span><input name="acceleration" placeholder="3.2s" /></label> <label><span data-i18n="adminAccel">0-100</span><input name="acceleration" placeholder="3.2s" /></label>
</div> </div>
<div class="row3"> <div class="row3">
<label><span>Sitze</span><input type="number" name="seats" min="1" value="2" /></label> <label><span data-i18n="adminSeats">Sitze</span><input type="number" name="seats" min="1" value="2" /></label>
<label><span>Preis / Tag (€)</span><input type="number" name="daily_price_eur" min="0" required /></label> <label><span data-i18n="adminPrice">Preis / Tag (€)</span><input type="number" name="daily_price_eur" min="0" required /></label>
<label><span>Reihenfolge</span><input type="number" name="sort_order" value="100" /></label> <label><span data-i18n="adminSort">Reihenfolge</span><input type="number" name="sort_order" value="100" /></label>
</div> </div>
<label> <label>
<span>Standort</span> <span data-i18n="adminLocation">Standort</span>
<input name="location" value="Steiermark (TBD)" /> <input name="location" value="Steiermark (TBD)" />
</label> </label>
<label> <label>
<span>Beschreibung (Deutsch)</span> <span data-i18n="adminDescDe">Beschreibung (Deutsch)</span>
<textarea name="description_de" rows="3"></textarea> <textarea name="description_de" rows="3"></textarea>
</label> </label>
<label> <label>
<span>Description (English)</span> <span data-i18n="adminDescEn">Description (English)</span>
<textarea name="description_en" rows="3"></textarea> <textarea name="description_en" rows="3"></textarea>
</label> </label>
<label style="flex-direction:row;align-items:center;gap:0.5rem;"> <label style="flex-direction:row;align-items:center;gap:0.8rem;margin-top:0.5rem;cursor:pointer;">
<input type="checkbox" name="is_active" checked style="width:auto;" /> <div class="toggle-switch">
<span>Aktiv / auf Website sichtbar</span> <input type="checkbox" name="is_active" id="isActiveCheck" checked />
<span class="toggle-slider"></span>
</div>
<span data-i18n="adminActiveVisible" style="user-select:none;">Aktiv / auf Website sichtbar</span>
</label> </label>
<div style="display:flex;gap:0.5rem;"> <div style="display:flex;gap:0.5rem;margin-top:1rem;">
<button class="btn" type="submit" id="saveBtn">Speichern</button> <button class="btn" type="submit" id="saveBtn" data-i18n="adminSave">Speichern</button>
<button class="btn ghost" type="button" id="resetBtn">Neu</button> <button class="btn ghost" type="button" id="resetBtn" data-i18n="adminReset">Neu</button>
</div> </div>
<p class="form-feedback" id="formFeedback"></p> <p class="form-feedback" id="formFeedback"></p>
</form> </form>
</div> </div>
<div class="panel"> <div class="panel">
<h2>Alle Fahrzeuge</h2> <h2 data-i18n="adminAllVehicles">Alle Fahrzeuge</h2>
<table class="admin-table" id="adminTable"> <table class="admin-table" id="adminTable">
<thead> <thead>
<tr> <tr>
<th>Foto</th> <th data-i18n="adminPhoto">Foto</th>
<th>Marke / Modell</th> <th data-i18n="adminBrandTable">Marke / Modell</th>
<th>€ / Tag</th> <th data-i18n="adminPriceTable">€ / Tag</th>
<th>Aktiv</th> <th data-i18n="adminActive">Aktiv</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
+27 -10
View File
@@ -1,4 +1,5 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; 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_URL = window.MCCARS_CONFIG?.SUPABASE_URL ?? "";
const SUPA_KEY = window.MCCARS_CONFIG?.SUPABASE_ANON_KEY || ""; const SUPA_KEY = window.MCCARS_CONFIG?.SUPABASE_ANON_KEY || "";
@@ -12,6 +13,7 @@ const supabase = createClient(SUPA_URL, SUPA_KEY, {
// ----- DOM ----- // ----- DOM -----
const loginView = document.querySelector("#loginView"); const loginView = document.querySelector("#loginView");
const adminView = document.querySelector("#adminView"); const adminView = document.querySelector("#adminView");
const langToggle = document.querySelector(".lang-toggle");
const rotateView = document.querySelector("#rotateView"); const rotateView = document.querySelector("#rotateView");
const loginForm = document.querySelector("#loginForm"); const loginForm = document.querySelector("#loginForm");
const loginError = document.querySelector("#loginError"); const loginError = document.querySelector("#loginError");
@@ -59,6 +61,7 @@ const state = {
// AUTH FLOW // AUTH FLOW
// ========================================================================= // =========================================================================
async function bootstrap() { async function bootstrap() {
applyI18n();
const { data: { session } } = await supabase.auth.getSession(); const { data: { session } } = await supabase.auth.getSession();
if (session) { if (session) {
// Always fetch fresh user from server so metadata (must_change_password) is current. // Always fetch fresh user from server so metadata (must_change_password) is current.
@@ -204,8 +207,8 @@ function renderVehicles() {
<td>€ ${v.daily_price_eur}</td> <td>€ ${v.daily_price_eur}</td>
<td>${v.is_active ? "✅" : "—"}</td> <td>${v.is_active ? "✅" : "—"}</td>
<td style="white-space:nowrap;"> <td style="white-space:nowrap;">
<button class="btn small ghost" data-edit="${v.id}">Edit</button> <button class="btn small ghost" data-edit="${v.id}">${t("editVehicle")}</button>
<button class="btn small danger" data-del="${v.id}">Del</button> <button class="btn small danger" data-del="${v.id}">${t("adminDel")}</button>
</td>`; </td>`;
tableBody.appendChild(tr); tableBody.appendChild(tr);
} }
@@ -352,12 +355,12 @@ function renderLeads() {
<td>${esc(l.date_from || "—")}${esc(l.date_to || "—")}</td> <td>${esc(l.date_from || "—")}${esc(l.date_to || "—")}</td>
<td><span class="pill pill-${esc(l.status)}">${esc(l.status)}</span></td> <td><span class="pill pill-${esc(l.status)}">${esc(l.status)}</span></td>
<td style="white-space:nowrap;"> <td style="white-space:nowrap;">
<button class="btn small ghost" data-open="${l.id}">Details</button> <button class="btn small ghost" data-open="${l.id}">${t("adminDetails")}</button>
${wantActive ? ` ${wantActive ? `
<button class="btn small" data-qual="${l.id}">Qualifizieren</button> <button class="btn small" data-qual="${l.id}">${t("adminQualify")}</button>
<button class="btn small danger" data-disq="${l.id}">Ablehnen</button> <button class="btn small danger" data-disq="${l.id}">${t("adminReject")}</button>
` : ` ` : `
<button class="btn small ghost" data-reopen="${l.id}">Wieder oeffnen</button> <button class="btn small ghost" data-reopen="${l.id}">${t("adminReopen")}</button>
`} `}
</td>`; </td>`;
leadsTableBody.appendChild(tr); leadsTableBody.appendChild(tr);
@@ -385,9 +388,9 @@ function openLead(id) {
</dl> </dl>
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.8rem;"> <div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.8rem;">
${l.is_active ? ` ${l.is_active ? `
<button class="btn danger" id="dlgDisq">Ablehnen</button> <button class="btn danger" id="dlgDisq">${t("adminReject")}</button>
<button class="btn" id="dlgQual">Qualifizieren</button> <button class="btn" id="dlgQual">${t("adminQualify")}</button>
` : `<button class="btn ghost" id="dlgReopen">Wieder oeffnen</button>`} ` : `<button class="btn ghost" id="dlgReopen">${t("adminReopen")}</button>`}
</div>`; </div>`;
leadDialog.showModal(); leadDialog.showModal();
const note = () => document.querySelector("#leadNote").value; const note = () => document.querySelector("#leadNote").value;
@@ -448,7 +451,7 @@ function renderCustomers() {
<td><span class="pill pill-${esc(c.status)}">${esc(c.status)}</span></td> <td><span class="pill pill-${esc(c.status)}">${esc(c.status)}</span></td>
<td style="white-space:nowrap;"> <td style="white-space:nowrap;">
<button class="btn small ghost" data-toggle="${c.id}" data-status="${c.status}"> <button class="btn small ghost" data-toggle="${c.id}" data-status="${c.status}">
${c.status === "active" ? "Inaktiv setzen" : "Aktiv setzen"} ${c.status === "active" ? t("adminSetInactive") : t("adminSetActive")}
</button> </button>
</td>`; </td>`;
customersTableBody.appendChild(tr); customersTableBody.appendChild(tr);
@@ -493,4 +496,18 @@ function fmtDate(iso) {
return d.toLocaleString("de-AT", { dateStyle: "short", timeStyle: "short" }); 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(); bootstrap();
+94 -2
View File
@@ -69,11 +69,57 @@ export const translations = {
footerNav: "Navigation", footerNav: "Navigation",
imprint: "Impressum", imprint: "Impressum",
privacy: "Datenschutz", privacy: "Datenschutz",
terms: "Mietbedingungen", footerTerms: "Mietbedingungen",
copyright: "Alle Rechte vorbehalten.", copyright: "Alle Rechte vorbehalten.",
close: "Schliessen", close: "Schliessen",
editVehicle: "Fahrzeug bearbeiten", 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: { en: {
navCars: "Fleet", navCars: "Fleet",
@@ -144,11 +190,57 @@ export const translations = {
footerNav: "Navigation", footerNav: "Navigation",
imprint: "Imprint", imprint: "Imprint",
privacy: "Privacy", privacy: "Privacy",
terms: "Rental conditions", footerTerms: "Rental conditions",
copyright: "All rights reserved.", copyright: "All rights reserved.",
close: "Close", close: "Close",
editVehicle: "Edit vehicle", 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",
}, },
}; };
+43
View File
@@ -604,6 +604,49 @@ table.admin-table tbody tr:hover {
filter: brightness(1.1); 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 */
.admin-tabs { .admin-tabs {
display: flex; gap: 0.4rem; display: flex; gap: 0.4rem;