feat(admin): replace checkbox with toggle-switch slider, add i18n multilanguage
This commit is contained in:
+48
-44
@@ -61,19 +61,20 @@
|
||||
<div class="admin-bar">
|
||||
<h1>MC Cars · Admin</h1>
|
||||
<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>
|
||||
<button id="changePwBtn" class="btn ghost small">Passwort aendern</button>
|
||||
<button id="logoutBtn" class="btn small">Logout</button>
|
||||
<span id="adminWho" style="color:var(--muted);font-size:0.85rem;margin-left:1rem;"></span>
|
||||
<button id="changePwBtn" class="btn ghost small" data-i18n="adminChangePw">Passwort aendern</button>
|
||||
<button id="logoutBtn" class="btn small" data-i18n="adminLogout">Logout</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<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" data-tab="customers" role="tab">Kunden <span id="customersBadge" class="tab-badge">0</span></button>
|
||||
<button class="tab" data-tab="vehicles" role="tab">Fahrzeuge</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"><span data-i18n="adminCustomers">Kunden</span> <span id="customersBadge" class="tab-badge">0</span></button>
|
||||
<button class="tab" data-tab="vehicles" role="tab" data-i18n="adminVehicles">Fahrzeuge</button>
|
||||
</div>
|
||||
|
||||
<!-- LEADS -->
|
||||
@@ -82,18 +83,18 @@
|
||||
<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>
|
||||
<div class="sub-tabs" role="tablist">
|
||||
<button class="sub-tab active" data-lview="active">Aktive Leads</button>
|
||||
<button class="sub-tab" data-lview="inactive">Abgeschlossen</button>
|
||||
<button class="sub-tab active" data-lview="active" data-i18n="adminActiveLeads">Aktive Leads</button>
|
||||
<button class="sub-tab" data-lview="inactive" data-i18n="adminClosedLeads">Abgeschlossen</button>
|
||||
</div>
|
||||
</div>
|
||||
<table class="admin-table" id="leadsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Eingang</th>
|
||||
<th>Name / E-Mail</th>
|
||||
<th>Fahrzeug</th>
|
||||
<th>Zeitraum</th>
|
||||
<th>Status</th>
|
||||
<th data-i18n="adminReceived">Eingang</th>
|
||||
<th data-i18n="adminNameEmail">Name / E-Mail</th>
|
||||
<th data-i18n="adminVehicleTab">Fahrzeug</th>
|
||||
<th data-i18n="adminPeriod">Zeitraum</th>
|
||||
<th data-i18n="adminStatus">Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -111,11 +112,11 @@
|
||||
<table class="admin-table" id="customersTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Erster Kontakt</th>
|
||||
<th>Name / E-Mail</th>
|
||||
<th>Telefon</th>
|
||||
<th>Quelle (Lead)</th>
|
||||
<th>Status</th>
|
||||
<th data-i18n="adminFirstContact">Erster Kontakt</th>
|
||||
<th data-i18n="adminNameEmail">Name / E-Mail</th>
|
||||
<th data-i18n="adminPhone">Telefon</th>
|
||||
<th data-i18n="adminSourceLead">Quelle (Lead)</th>
|
||||
<th data-i18n="adminStatus">Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -129,73 +130,76 @@
|
||||
<div class="tab-panel" id="tab-vehicles" style="display:none;">
|
||||
<div class="admin-grid">
|
||||
<div class="panel">
|
||||
<h2 id="formTitle">Neues Fahrzeug</h2>
|
||||
<h2 id="formTitle" data-i18n="adminNewVehicle">Neues Fahrzeug</h2>
|
||||
<form class="admin-form" id="vehicleForm">
|
||||
<input type="hidden" name="vid" />
|
||||
|
||||
<div class="admin-photo-preview" id="photoPreview"></div>
|
||||
<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/*" />
|
||||
</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://..." />
|
||||
</label>
|
||||
|
||||
<div class="row2">
|
||||
<label><span>Marke</span><input name="brand" required /></label>
|
||||
<label><span>Modell</span><input name="model" 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>
|
||||
</div>
|
||||
|
||||
<div class="row3">
|
||||
<label><span>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>0-100</span><input name="acceleration" placeholder="3.2s" /></label>
|
||||
<label><span data-i18n="adminPower">PS</span><input type="number" name="power_hp" 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 data-i18n="adminAccel">0-100</span><input name="acceleration" placeholder="3.2s" /></label>
|
||||
</div>
|
||||
|
||||
<div class="row3">
|
||||
<label><span>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>Reihenfolge</span><input type="number" name="sort_order" value="100" /></label>
|
||||
<label><span data-i18n="adminSeats">Sitze</span><input type="number" name="seats" min="1" value="2" /></label>
|
||||
<label><span data-i18n="adminPrice">Preis / Tag (€)</span><input type="number" name="daily_price_eur" min="0" required /></label>
|
||||
<label><span data-i18n="adminSort">Reihenfolge</span><input type="number" name="sort_order" value="100" /></label>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<span>Standort</span>
|
||||
<span data-i18n="adminLocation">Standort</span>
|
||||
<input name="location" value="Steiermark (TBD)" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Beschreibung (Deutsch)</span>
|
||||
<span data-i18n="adminDescDe">Beschreibung (Deutsch)</span>
|
||||
<textarea name="description_de" rows="3"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>Description (English)</span>
|
||||
<span data-i18n="adminDescEn">Description (English)</span>
|
||||
<textarea name="description_en" rows="3"></textarea>
|
||||
</label>
|
||||
|
||||
<label style="flex-direction:row;align-items:center;gap:0.5rem;">
|
||||
<input type="checkbox" name="is_active" checked style="width:auto;" />
|
||||
<span>Aktiv / auf Website sichtbar</span>
|
||||
<label style="flex-direction:row;align-items:center;gap:0.8rem;margin-top:0.5rem;cursor:pointer;">
|
||||
<div class="toggle-switch">
|
||||
<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>
|
||||
|
||||
<div style="display:flex;gap:0.5rem;">
|
||||
<button class="btn" type="submit" id="saveBtn">Speichern</button>
|
||||
<button class="btn ghost" type="button" id="resetBtn">Neu</button>
|
||||
<div style="display:flex;gap:0.5rem;margin-top:1rem;">
|
||||
<button class="btn" type="submit" id="saveBtn" data-i18n="adminSave">Speichern</button>
|
||||
<button class="btn ghost" type="button" id="resetBtn" data-i18n="adminReset">Neu</button>
|
||||
</div>
|
||||
<p class="form-feedback" id="formFeedback"></p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Alle Fahrzeuge</h2>
|
||||
<h2 data-i18n="adminAllVehicles">Alle Fahrzeuge</h2>
|
||||
<table class="admin-table" id="adminTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Foto</th>
|
||||
<th>Marke / Modell</th>
|
||||
<th>€ / Tag</th>
|
||||
<th>Aktiv</th>
|
||||
<th data-i18n="adminPhoto">Foto</th>
|
||||
<th data-i18n="adminBrandTable">Marke / Modell</th>
|
||||
<th data-i18n="adminPriceTable">€ / Tag</th>
|
||||
<th data-i18n="adminActive">Aktiv</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
+27
-10
@@ -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() {
|
||||
<td>€ ${v.daily_price_eur}</td>
|
||||
<td>${v.is_active ? "✅" : "—"}</td>
|
||||
<td style="white-space:nowrap;">
|
||||
<button class="btn small ghost" data-edit="${v.id}">Edit</button>
|
||||
<button class="btn small danger" data-del="${v.id}">Del</button>
|
||||
<button class="btn small ghost" data-edit="${v.id}">${t("editVehicle")}</button>
|
||||
<button class="btn small danger" data-del="${v.id}">${t("adminDel")}</button>
|
||||
</td>`;
|
||||
tableBody.appendChild(tr);
|
||||
}
|
||||
@@ -352,12 +355,12 @@ function renderLeads() {
|
||||
<td>${esc(l.date_from || "—")} → ${esc(l.date_to || "—")}</td>
|
||||
<td><span class="pill pill-${esc(l.status)}">${esc(l.status)}</span></td>
|
||||
<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 ? `
|
||||
<button class="btn small" data-qual="${l.id}">Qualifizieren</button>
|
||||
<button class="btn small danger" data-disq="${l.id}">Ablehnen</button>
|
||||
<button class="btn small" data-qual="${l.id}">${t("adminQualify")}</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>`;
|
||||
leadsTableBody.appendChild(tr);
|
||||
@@ -385,9 +388,9 @@ function openLead(id) {
|
||||
</dl>
|
||||
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.8rem;">
|
||||
${l.is_active ? `
|
||||
<button class="btn danger" id="dlgDisq">Ablehnen</button>
|
||||
<button class="btn" id="dlgQual">Qualifizieren</button>
|
||||
` : `<button class="btn ghost" id="dlgReopen">Wieder oeffnen</button>`}
|
||||
<button class="btn danger" id="dlgDisq">${t("adminReject")}</button>
|
||||
<button class="btn" id="dlgQual">${t("adminQualify")}</button>
|
||||
` : `<button class="btn ghost" id="dlgReopen">${t("adminReopen")}</button>`}
|
||||
</div>`;
|
||||
leadDialog.showModal();
|
||||
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 style="white-space:nowrap;">
|
||||
<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>
|
||||
</td>`;
|
||||
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();
|
||||
|
||||
+94
-2
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user