feat: add RPC for public lead creation and update migrations in docker-compose files
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -19,6 +19,7 @@ services:
|
|||||||
- ./supabase/migrations/02-leads.sql:/sql/02-leads.sql:ro
|
- ./supabase/migrations/02-leads.sql:/sql/02-leads.sql:ro
|
||||||
- ./supabase/migrations/03-booking-flow.sql:/sql/03-booking-flow.sql:ro
|
- ./supabase/migrations/03-booking-flow.sql:/sql/03-booking-flow.sql:ro
|
||||||
- ./supabase/migrations/04-kaution-weekend-km.sql:/sql/04-kaution-weekend-km.sql:ro
|
- ./supabase/migrations/04-kaution-weekend-km.sql:/sql/04-kaution-weekend-km.sql:ro
|
||||||
|
- ./supabase/migrations/05-create-lead-rpc.sql:/sql/05-create-lead-rpc.sql:ro
|
||||||
|
|
||||||
kong:
|
kong:
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ services:
|
|||||||
- /mnt/user/appdata/mc-cars/supabase/migrations/02-leads.sql:/sql/02-leads.sql:ro
|
- /mnt/user/appdata/mc-cars/supabase/migrations/02-leads.sql:/sql/02-leads.sql:ro
|
||||||
- /mnt/user/appdata/mc-cars/supabase/migrations/03-booking-flow.sql:/sql/03-booking-flow.sql:ro
|
- /mnt/user/appdata/mc-cars/supabase/migrations/03-booking-flow.sql:/sql/03-booking-flow.sql:ro
|
||||||
- /mnt/user/appdata/mc-cars/supabase/migrations/04-kaution-weekend-km.sql:/sql/04-kaution-weekend-km.sql:ro
|
- /mnt/user/appdata/mc-cars/supabase/migrations/04-kaution-weekend-km.sql:/sql/04-kaution-weekend-km.sql:ro
|
||||||
|
- /mnt/user/appdata/mc-cars/supabase/migrations/05-create-lead-rpc.sql:/sql/05-create-lead-rpc.sql:ro
|
||||||
entrypoint: ["sh","-c"]
|
entrypoint: ["sh","-c"]
|
||||||
command:
|
command:
|
||||||
- |
|
- |
|
||||||
@@ -232,6 +233,7 @@ services:
|
|||||||
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/02-leads.sql
|
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/02-leads.sql
|
||||||
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/03-booking-flow.sql
|
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/03-booking-flow.sql
|
||||||
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/04-kaution-weekend-km.sql
|
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/04-kaution-weekend-km.sql
|
||||||
|
psql "postgresql://postgres:$$PGPASSWORD@db:5432/postgres" -v ON_ERROR_STOP=1 -f /sql/05-create-lead-rpc.sql
|
||||||
echo "post-init done."
|
echo "post-init done."
|
||||||
restart: "no"
|
restart: "no"
|
||||||
networks: [mccars]
|
networks: [mccars]
|
||||||
|
|||||||
+2
-2
@@ -65,7 +65,7 @@
|
|||||||
<button class="lang-toggle" type="button" aria-label="Sprache wechseln" style="margin-left:auto;">EN</button>
|
<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;margin-left:1rem;"></span>
|
<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="changePwBtn" class="btn ghost small" data-i18n="adminChangePw">Passwort ändern</button>
|
||||||
<button id="logoutBtn" class="btn small" data-i18n="adminLogout">Logout</button>
|
<button id="logoutBtn" class="btn small" data-i18n="adminLogout">Logout</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<div class="tab-panel" id="tab-customers" style="display:none;">
|
<div class="tab-panel" id="tab-customers" style="display:none;">
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Kunden</h2>
|
<h2>Kunden</h2>
|
||||||
<p class="muted" style="margin-top:-0.4rem;">Entstehen automatisch, sobald ein Lead qualifiziert wird. Die Quelle bleibt als <code>lead_id</code> verknuepft.</p>
|
<p class="muted" style="margin-top:-0.4rem;">Entstehen automatisch, sobald ein Lead qualifiziert wird. Die Quelle bleibt als <code>lead_id</code> verknüpft.</p>
|
||||||
<table class="admin-table" id="customersTable">
|
<table class="admin-table" id="customersTable">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
+1
-1
@@ -104,7 +104,7 @@ rotateForm.addEventListener("submit", async (e) => {
|
|||||||
const fd = new FormData(rotateForm);
|
const fd = new FormData(rotateForm);
|
||||||
const pw1 = fd.get("pw1");
|
const pw1 = fd.get("pw1");
|
||||||
const pw2 = fd.get("pw2");
|
const pw2 = fd.get("pw2");
|
||||||
if (pw1 !== pw2) { rotateError.textContent = "Passwoerter stimmen nicht ueberein."; return; }
|
if (pw1 !== pw2) { rotateError.textContent = "Passwörter stimmen nicht überein."; return; }
|
||||||
if (pw1.length < 10) { rotateError.textContent = "Mindestens 10 Zeichen."; return; }
|
if (pw1.length < 10) { rotateError.textContent = "Mindestens 10 Zeichen."; return; }
|
||||||
if (state.loginPassword && pw1 === state.loginPassword) {
|
if (state.loginPassword && pw1 === state.loginPassword) {
|
||||||
rotateError.textContent = "Neues Passwort muss sich vom alten unterscheiden.";
|
rotateError.textContent = "Neues Passwort muss sich vom alten unterscheiden.";
|
||||||
|
|||||||
+26
-18
@@ -48,10 +48,12 @@ const bpfPhone = document.querySelector("#bpfPhone");
|
|||||||
const bpfMessage = document.querySelector("#bpfMessage");
|
const bpfMessage = document.querySelector("#bpfMessage");
|
||||||
const bpfFileId = document.querySelector("#bpfFileId");
|
const bpfFileId = document.querySelector("#bpfFileId");
|
||||||
const bpfFileIncome = document.querySelector("#bpfFileIncome");
|
const bpfFileIncome = document.querySelector("#bpfFileIncome");
|
||||||
|
const bpfSubmitBtn = document.querySelector("#bpfSubmit");
|
||||||
const bpfSidebar = document.querySelector("#bpfSidebar");
|
const bpfSidebar = document.querySelector("#bpfSidebar");
|
||||||
const bpfSidebarContent = document.querySelector("#bpfSidebarContent");
|
const bpfSidebarContent = document.querySelector("#bpfSidebarContent");
|
||||||
const bpfSidebarPlaceholder = document.querySelector(".bpf-sidebar-placeholder");
|
const bpfSidebarPlaceholder = document.querySelector(".bpf-sidebar-placeholder");
|
||||||
let bpfDurationMode = "custom"; // "day" | "weekend" | "custom"
|
let bpfDurationMode = "custom"; // "day" | "weekend" | "custom"
|
||||||
|
let bpfSubmitting = false;
|
||||||
|
|
||||||
function formatYmdLocal(d) {
|
function formatYmdLocal(d) {
|
||||||
const y = d.getFullYear();
|
const y = d.getFullYear();
|
||||||
@@ -220,21 +222,20 @@ function openDetails(id) {
|
|||||||
|
|
||||||
// ---------------- Reviews ----------------
|
// ---------------- Reviews ----------------
|
||||||
function renderReviews() {
|
function renderReviews() {
|
||||||
const list = REVIEWS[getLang()];
|
state.reviewIdx = state.reviewIdx % REVIEWS.length;
|
||||||
state.reviewIdx = state.reviewIdx % list.length;
|
const r = REVIEWS[state.reviewIdx];
|
||||||
const r = list[state.reviewIdx];
|
|
||||||
reviewStrip.innerHTML = `
|
reviewStrip.innerHTML = `
|
||||||
<p class="review-quote">"${escapeHtml(r.quote)}"</p>
|
<p class="review-quote">"${escapeHtml(r.quote)}"</p>
|
||||||
<p class="review-author">${escapeHtml(r.author)}</p>
|
<p class="review-author">${escapeHtml(r.author)}</p>
|
||||||
`;
|
`;
|
||||||
reviewDots.innerHTML = list.map((_, i) =>
|
reviewDots.innerHTML = REVIEWS.map((_, i) =>
|
||||||
`<button class="${i === state.reviewIdx ? 'active' : ''}" data-rev="${i}" aria-label="${t("review")} ${i + 1}"></button>`
|
`<button class="${i === state.reviewIdx ? 'active' : ''}" data-rev="${i}" aria-label="${t("review")} ${i + 1}"></button>`
|
||||||
).join("");
|
).join("");
|
||||||
reviewDots.querySelectorAll("button").forEach(b => {
|
reviewDots.querySelectorAll("button").forEach(b => {
|
||||||
b.addEventListener("click", () => { state.reviewIdx = +b.dataset.rev; renderReviews(); });
|
b.addEventListener("click", () => { state.reviewIdx = +b.dataset.rev; renderReviews(); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setInterval(() => { state.reviewIdx++; renderReviews(); }, 6000);
|
setInterval(() => { state.reviewIdx++; renderReviews(); }, 5000);
|
||||||
|
|
||||||
// ---------------- BPF WIZARD ----------------
|
// ---------------- BPF WIZARD ----------------
|
||||||
const bpfStepPanels = [
|
const bpfStepPanels = [
|
||||||
@@ -402,38 +403,43 @@ bpfTo.addEventListener("change", updateSidebar);
|
|||||||
|
|
||||||
// Submit BPF
|
// Submit BPF
|
||||||
document.querySelector("#bpfSubmit").addEventListener("click", async () => {
|
document.querySelector("#bpfSubmit").addEventListener("click", async () => {
|
||||||
|
if (bpfSubmitting) return;
|
||||||
|
bpfSubmitting = true;
|
||||||
|
if (bpfSubmitBtn) bpfSubmitBtn.disabled = true;
|
||||||
bookingFeedback.className = "form-feedback";
|
bookingFeedback.className = "form-feedback";
|
||||||
bookingFeedback.textContent = "...";
|
bookingFeedback.textContent = "...";
|
||||||
|
|
||||||
const vehicle = state.vehicles.find(v => v.id === bpfCar.value);
|
const vehicle = state.vehicles.find(v => v.id === bpfCar.value);
|
||||||
const payload = {
|
const payload = {
|
||||||
name: bpfName.value,
|
p_name: bpfName.value,
|
||||||
email: bpfEmail.value,
|
p_email: bpfEmail.value,
|
||||||
phone: bpfPhone.value || "",
|
p_phone: bpfPhone.value || "",
|
||||||
vehicle_id: bpfCar.value || null,
|
p_vehicle_id: bpfCar.value || null,
|
||||||
vehicle_label: vehicle ? `${vehicle.brand} ${vehicle.model}` : "",
|
p_vehicle_label: vehicle ? `${vehicle.brand} ${vehicle.model}` : "",
|
||||||
date_from: bpfFrom.value || null,
|
p_date_from: bpfFrom.value || null,
|
||||||
date_to: bpfTo.value || null,
|
p_date_to: bpfTo.value || null,
|
||||||
message: bpfMessage.value || "",
|
p_message: bpfMessage.value || "",
|
||||||
source: "website",
|
p_source: "website",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create lead
|
// Create lead via RPC (returns inserted id without anon SELECT privileges)
|
||||||
const { data: lead, error } = await supabase.from("leads").insert(payload).select("id").single();
|
const { data: leadId, error } = await supabase.rpc("create_lead", payload);
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
bookingFeedback.className = "form-feedback error";
|
bookingFeedback.className = "form-feedback error";
|
||||||
bookingFeedback.textContent = t("bookingFailed");
|
bookingFeedback.textContent = t("bookingFailed");
|
||||||
|
bpfSubmitting = false;
|
||||||
|
if (bpfSubmitBtn) bpfSubmitBtn.disabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload files
|
// Upload files
|
||||||
const uploads = [];
|
const uploads = [];
|
||||||
if (bpfFileId.files[0]) {
|
if (bpfFileId.files[0]) {
|
||||||
uploads.push(uploadDoc(lead.id, bpfFileId.files[0], "id_document"));
|
uploads.push(uploadDoc(leadId, bpfFileId.files[0], "id_document"));
|
||||||
}
|
}
|
||||||
if (bpfFileIncome.files[0]) {
|
if (bpfFileIncome.files[0]) {
|
||||||
uploads.push(uploadDoc(lead.id, bpfFileIncome.files[0], "income_proof"));
|
uploads.push(uploadDoc(leadId, bpfFileIncome.files[0], "income_proof"));
|
||||||
}
|
}
|
||||||
await Promise.all(uploads);
|
await Promise.all(uploads);
|
||||||
|
|
||||||
@@ -453,6 +459,8 @@ document.querySelector("#bpfSubmit").addEventListener("click", async () => {
|
|||||||
document.querySelector("#bpfFileIncomeName").textContent = "";
|
document.querySelector("#bpfFileIncomeName").textContent = "";
|
||||||
setDurationMode("custom");
|
setDurationMode("custom");
|
||||||
updateSidebar();
|
updateSidebar();
|
||||||
|
bpfSubmitting = false;
|
||||||
|
if (bpfSubmitBtn) bpfSubmitBtn.disabled = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function uploadDoc(leadId, file, kind) {
|
async function uploadDoc(leadId, file, kind) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<main class="shell" style="padding:4rem 1rem;">
|
<main class="shell" style="padding:4rem 1rem;">
|
||||||
<p class="eyebrow">Rechtliches</p>
|
<p class="eyebrow">Rechtliches</p>
|
||||||
<h1>Datenschutz</h1>
|
<h1>Datenschutz</h1>
|
||||||
<p>Buchungsanfragen werden aktuell zu Demozwecken lokal im Browser gespeichert. Fahrzeugdaten werden ueber ein selbstgehostetes Supabase verwaltet.</p>
|
<p>Buchungsanfragen werden aktuell zu Demozwecken lokal im Browser gespeichert. Fahrzeugdaten werden über ein selbstgehostetes Supabase verwaltet.</p>
|
||||||
<p>Ansprechpartner: hello@mccars.at</p>
|
<p>Ansprechpartner: hello@mccars.at</p>
|
||||||
<p style="margin-top:2rem;"><a class="btn small" href="index.html">← Startseite</a></p>
|
<p style="margin-top:2rem;"><a class="btn small" href="index.html">← Startseite</a></p>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+31
-36
@@ -9,7 +9,7 @@ export const translations = {
|
|||||||
viewFleet: "Flotte ansehen",
|
viewFleet: "Flotte ansehen",
|
||||||
|
|
||||||
heroEyebrow: "MC Cars · Sportwagenvermietung",
|
heroEyebrow: "MC Cars · Sportwagenvermietung",
|
||||||
heroTitle: "Fahren auf hoechstem Niveau.",
|
heroTitle: "Fahren auf höchstem Niveau.",
|
||||||
heroLead: "Premium-Sportwagen und Luxusklasse in der Steiermark. Faire Kaution, transparent, sofort startklar.",
|
heroLead: "Premium-Sportwagen und Luxusklasse in der Steiermark. Faire Kaution, transparent, sofort startklar.",
|
||||||
|
|
||||||
statDeposit: "Faire Kaution",
|
statDeposit: "Faire Kaution",
|
||||||
@@ -18,7 +18,7 @@ export const translations = {
|
|||||||
|
|
||||||
fleetEyebrow: "Unsere Flotte",
|
fleetEyebrow: "Unsere Flotte",
|
||||||
fleetTitle: "Handverlesen. Gepflegt. Startklar.",
|
fleetTitle: "Handverlesen. Gepflegt. Startklar.",
|
||||||
fleetSub: "Filtern Sie nach Marke und Preis. Klicken Sie fuer Details oder buchen Sie direkt.",
|
fleetSub: "Filtern Sie nach Marke und Preis. Klicken Sie für Details oder buchen Sie direkt.",
|
||||||
filterBrand: "Marke",
|
filterBrand: "Marke",
|
||||||
filterSort: "Sortierung",
|
filterSort: "Sortierung",
|
||||||
filterPrice: "Max. Preis / Tag",
|
filterPrice: "Max. Preis / Tag",
|
||||||
@@ -37,13 +37,13 @@ export const translations = {
|
|||||||
noMatches: "Keine Fahrzeuge gefunden.",
|
noMatches: "Keine Fahrzeuge gefunden.",
|
||||||
|
|
||||||
whyEyebrow: "Warum MC Cars",
|
whyEyebrow: "Warum MC Cars",
|
||||||
whyTitle: "Keine Kompromisse zwischen Sicherheit und Fahrspass.",
|
whyTitle: "Keine Kompromisse zwischen Sicherheit und Fahrspaß.",
|
||||||
whyInsurance: "Versicherungsschutz",
|
whyInsurance: "Versicherungsschutz",
|
||||||
whyInsuranceText: "Vollkasko mit klarem Selbstbehalt. Transparente Kosten auf jedem Kilometer.",
|
whyInsuranceText: "Vollkasko mit klarem Selbstbehalt. Transparente Kosten auf jedem Kilometer.",
|
||||||
whyFleet: "Premium Flotte",
|
whyFleet: "Premium Flotte",
|
||||||
whyFleetText: "Handverlesene Performance-Modelle, professionell gewartet und sofort startklar.",
|
whyFleetText: "Handverlesene Performance-Modelle, professionell gewartet und sofort startklar.",
|
||||||
whyDeposit: "Faire Kaution",
|
whyDeposit: "Faire Kaution",
|
||||||
whyDepositText: "Kein Ueberziehen. Transparente, faire Kaution ohne unnoetige Belastung.",
|
whyDepositText: "Zwei Kautionsarten: Bar oder PayPal-Kaution. Bei PayPal senden wir einen Deposit-Link. Bar wird aktuell persönlich bei der Fahrzeugübergabe abgewickelt.",
|
||||||
|
|
||||||
reviewsEyebrow: "Kundenmeinungen",
|
reviewsEyebrow: "Kundenmeinungen",
|
||||||
reviewsTitle: "Erlebnisse, die bleiben.",
|
reviewsTitle: "Erlebnisse, die bleiben.",
|
||||||
@@ -58,50 +58,50 @@ export const translations = {
|
|||||||
fieldFrom: "Von",
|
fieldFrom: "Von",
|
||||||
fieldTo: "Bis",
|
fieldTo: "Bis",
|
||||||
fieldMessage: "Nachricht",
|
fieldMessage: "Nachricht",
|
||||||
messagePlaceholder: "Wuensche, Uhrzeit, Anlass...",
|
messagePlaceholder: "Wünsche, Uhrzeit, Anlass...",
|
||||||
sendRequest: "Anfrage senden",
|
sendRequest: "Anfrage senden",
|
||||||
invalidDates: "Bitte ein gueltiges Datum waehlen (Bis > Von).",
|
invalidDates: "Bitte ein gültiges Datum wählen (Bis > Von).",
|
||||||
weekendSaturdayOnly: "Im Wochenendmodus bitte einen Samstag waehlen.",
|
weekendSaturdayOnly: "Im Wochenendmodus bitte einen Samstag wählen.",
|
||||||
bookingSuccess: "Danke! Wir melden uns in Kuerze per E-Mail.",
|
bookingSuccess: "Danke! Wir melden uns in Kürze per E-Mail.",
|
||||||
bookingFailed: "Anfrage konnte nicht gesendet werden. Bitte erneut versuchen.",
|
bookingFailed: "Anfrage konnte nicht gesendet werden. Bitte erneut versuchen.",
|
||||||
|
|
||||||
// BPF Wizard
|
// BPF Wizard
|
||||||
bpfTitle: "Jetzt buchen",
|
bpfTitle: "Jetzt buchen",
|
||||||
bpfSubtitle: "Waehle dein Wunschfahrzeug, den Zeitraum und konfiguriere deine Buchung nach Wunsch.",
|
bpfSubtitle: "Wähle dein Wunschfahrzeug, den Zeitraum und konfiguriere deine Buchung nach Wunsch.",
|
||||||
stepVehicleTime: "Fahrzeug & Zeitraum",
|
stepVehicleTime: "Fahrzeug & Zeitraum",
|
||||||
stepContact: "Kontaktdaten",
|
stepContact: "Kontaktdaten",
|
||||||
stepVerification: "ID-Verifizierung",
|
stepVerification: "ID-Verifizierung",
|
||||||
bpfRentalDuration: "Mietdauer",
|
bpfRentalDuration: "Mietdauer",
|
||||||
bpfVehicle: "Fahrzeug",
|
bpfVehicle: "Fahrzeug",
|
||||||
bpfSelectVehicle: "Fahrzeug waehlen",
|
bpfSelectVehicle: "Fahrzeug wählen",
|
||||||
bpfDuration: "Mietdauer",
|
bpfDuration: "Mietdauer",
|
||||||
bpfPresetDay: "1 Tag",
|
bpfPresetDay: "1 Tag",
|
||||||
bpfPresetWeekend: "Wochenende",
|
bpfPresetWeekend: "Wochenende",
|
||||||
bpfPresetCustom: "Individuell",
|
bpfPresetCustom: "Individuell",
|
||||||
bpfPickDate: "Datum waehlen",
|
bpfPickDate: "Datum wählen",
|
||||||
bpfPickWeekend: "Wochenende waehlen (Samstag)",
|
bpfPickWeekend: "Wochenende wählen (Samstag)",
|
||||||
bpfStartDate: "Startdatum",
|
bpfStartDate: "Startdatum",
|
||||||
bpfEndDate: "Enddatum",
|
bpfEndDate: "Enddatum",
|
||||||
bpfSelectDate: "Datum waehlen",
|
bpfSelectDate: "Datum wählen",
|
||||||
bpfNext: "Weiter",
|
bpfNext: "Weiter",
|
||||||
bpfBack: "Zurueck",
|
bpfBack: "Zurück",
|
||||||
bpfDailyRate: "Tagesmiete",
|
bpfDailyRate: "Tagesmiete",
|
||||||
bpfWeekendRate: "Wochenendmiete",
|
bpfWeekendRate: "Wochenendmiete",
|
||||||
bpfWeekendDef: "Wochenende: Samstag 9:00 – Sonntag 20:00",
|
bpfWeekendDef: "Wochenende: Samstag 9:00 – Sonntag 20:00",
|
||||||
bpfMaxKm: "Max. km/Tag",
|
bpfMaxKm: "Max. km/Tag",
|
||||||
bpfExtraKm: "Extra km",
|
bpfExtraKm: "Extra km",
|
||||||
bpfPriceOverview: "Preisuebersicht",
|
bpfPriceOverview: "Preisübersicht",
|
||||||
bpfSelectForPrice: "Waehle Fahrzeug und Datum fuer eine Preisuebersicht",
|
bpfSelectForPrice: "Wähle Fahrzeug und Datum für eine Preisübersicht",
|
||||||
bpfSubtotal: "Zwischensumme",
|
bpfSubtotal: "Zwischensumme",
|
||||||
bpfVat: "MwSt. (20%)",
|
bpfVat: "MwSt. (20%)",
|
||||||
bpfTotal: "Gesamtbetrag",
|
bpfTotal: "Gesamtbetrag",
|
||||||
bpfDeposit: "Kaution",
|
bpfDeposit: "Kaution",
|
||||||
bpfIncludedKm: "Inkludierte Kilometer",
|
bpfIncludedKm: "Inkludierte Kilometer",
|
||||||
bpfIdUpload: "Ausweis / Fuehrerschein",
|
bpfIdUpload: "Ausweis / Führerschein",
|
||||||
bpfIncomeUpload: "Lohnzettel / Gehaltsnachweis",
|
bpfIncomeUpload: "Lohnzettel / Gehaltsnachweis (optional)",
|
||||||
bpfUploadHint: "PDF, JPG, PNG (max. 10 MB)",
|
bpfUploadHint: "PDF, JPG, PNG (max. 10 MB)",
|
||||||
bpfClickUpload: "Klicken zum Hochladen",
|
bpfClickUpload: "Klicken zum Hochladen",
|
||||||
bpfIdNotice: "Ihre Dokumente werden vertraulich behandelt und dienen ausschliesslich der Identitaetsverifizierung.",
|
bpfIdNotice: "Ihre Dokumente werden vertraulich behandelt und dienen ausschließlich der Identitätsverifizierung.",
|
||||||
bpfSubmit: "Anfrage absenden",
|
bpfSubmit: "Anfrage absenden",
|
||||||
bpfPerKm: "/km",
|
bpfPerKm: "/km",
|
||||||
bpfDays: "Tage",
|
bpfDays: "Tage",
|
||||||
@@ -112,7 +112,7 @@ export const translations = {
|
|||||||
perWeekend: "Wochenende",
|
perWeekend: "Wochenende",
|
||||||
weekendDef: "Sa 9:00 – So 20:00",
|
weekendDef: "Sa 9:00 – So 20:00",
|
||||||
|
|
||||||
footerTagline: "Sportwagenvermietung in Oesterreich. Standort: Steiermark (TBD).",
|
footerTagline: "Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).",
|
||||||
footerLegal: "Rechtliches",
|
footerLegal: "Rechtliches",
|
||||||
footerContact: "Kontakt",
|
footerContact: "Kontakt",
|
||||||
footerNav: "Navigation",
|
footerNav: "Navigation",
|
||||||
@@ -121,11 +121,11 @@ export const translations = {
|
|||||||
footerTerms: "Mietbedingungen",
|
footerTerms: "Mietbedingungen",
|
||||||
copyright: "Alle Rechte vorbehalten.",
|
copyright: "Alle Rechte vorbehalten.",
|
||||||
|
|
||||||
close: "Schliessen",
|
close: "Schließen",
|
||||||
editVehicle: "Fahrzeug bearbeiten",
|
editVehicle: "Fahrzeug bearbeiten",
|
||||||
|
|
||||||
adminNavWebsite: "Website",
|
adminNavWebsite: "Website",
|
||||||
adminChangePw: "Passwort aendern",
|
adminChangePw: "Passwort ändern",
|
||||||
adminLogout: "Logout",
|
adminLogout: "Logout",
|
||||||
adminLeads: "Leads",
|
adminLeads: "Leads",
|
||||||
adminCustomers: "Kunden",
|
adminCustomers: "Kunden",
|
||||||
@@ -214,7 +214,7 @@ export const translations = {
|
|||||||
whyFleet: "Premium fleet",
|
whyFleet: "Premium fleet",
|
||||||
whyFleetText: "Hand-picked performance models, professionally maintained and ready to go.",
|
whyFleetText: "Hand-picked performance models, professionally maintained and ready to go.",
|
||||||
whyDeposit: "Fair Deposit",
|
whyDeposit: "Fair Deposit",
|
||||||
whyDepositText: "No overcharge. A transparent, fair deposit with no unnecessary burden.",
|
whyDepositText: "Two deposit options: cash or PayPal deposit. For PayPal, we send a deposit link. Cash is currently handled in person at pickup.",
|
||||||
|
|
||||||
reviewsEyebrow: "Testimonials",
|
reviewsEyebrow: "Testimonials",
|
||||||
reviewsTitle: "Experiences that last.",
|
reviewsTitle: "Experiences that last.",
|
||||||
@@ -269,7 +269,7 @@ export const translations = {
|
|||||||
bpfDeposit: "Deposit",
|
bpfDeposit: "Deposit",
|
||||||
bpfIncludedKm: "Included kilometers",
|
bpfIncludedKm: "Included kilometers",
|
||||||
bpfIdUpload: "ID / Driving license",
|
bpfIdUpload: "ID / Driving license",
|
||||||
bpfIncomeUpload: "Pay slip / Income proof",
|
bpfIncomeUpload: "Pay slip / Income proof (optional)",
|
||||||
bpfUploadHint: "PDF, JPG, PNG (max. 10 MB)",
|
bpfUploadHint: "PDF, JPG, PNG (max. 10 MB)",
|
||||||
bpfClickUpload: "Click to upload",
|
bpfClickUpload: "Click to upload",
|
||||||
bpfIdNotice: "Your documents are treated confidentially and are used exclusively for identity verification.",
|
bpfIdNotice: "Your documents are treated confidentially and are used exclusively for identity verification.",
|
||||||
@@ -344,18 +344,13 @@ export const translations = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const REVIEWS = {
|
export const REVIEWS = [
|
||||||
de: [
|
{ quote: "Die Buchung war klar und schnell. Der GT3 war in einem herausragenden Zustand.", author: "Martin P.", lang: "de" },
|
||||||
{ quote: "Top Service und perfekt vorbereitete Fahrzeuge. Unser Wochenendtrip war ein Highlight.", author: "Laura K." },
|
{ quote: "Exzellenter Service und makellos vorbereitete Fahrzeuge. Unser Wochenendtrip war unvergesslich.", author: "James R.", lang: "de" },
|
||||||
{ quote: "Die Buchung war klar und schnell. Der GT3 war in einem herausragenden Zustand.", author: "Martin P." },
|
{ quote: "Hervorragende Buchungsabwicklung und tadelloses Fahrzeugzustand. Sehr zufrieden.", author: "Thomas W.", lang: "de" },
|
||||||
{ quote: "Sehr professionelles Team und ehrliche Kommunikation zu allen Konditionen.", author: "Sina T." },
|
{ quote: "Professionelles Team und untadelige Aufmerksamkeit zum Detail. Sehr empfohlen.", author: "David M.", lang: "de" },
|
||||||
],
|
{ quote: "Booking was clear and fast. The GT3 arrived in outstanding condition.", author: "Jonas P.", lang: "en" },
|
||||||
en: [
|
];
|
||||||
{ quote: "Excellent service and flawlessly prepared cars. Our weekend trip was unforgettable.", author: "Laura K." },
|
|
||||||
{ quote: "Booking was clear and fast. The GT3 arrived in outstanding condition.", author: "Martin P." },
|
|
||||||
{ quote: "Very professional team and transparent communication on all terms.", author: "Sina T." },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export function getLang() {
|
export function getLang() {
|
||||||
return localStorage.getItem("mccars.lang") || "de";
|
return localStorage.getItem("mccars.lang") || "de";
|
||||||
|
|||||||
+18
-18
@@ -19,7 +19,7 @@
|
|||||||
<span>MC Cars</span>
|
<span>MC Cars</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button class="menu-toggle" aria-label="Menue">☰</button>
|
<button class="menu-toggle" aria-label="Menü">☰</button>
|
||||||
|
|
||||||
<nav class="main-nav" aria-label="Hauptnavigation">
|
<nav class="main-nav" aria-label="Hauptnavigation">
|
||||||
<a href="#fahrzeuge" data-i18n="navCars">Fahrzeuge</a>
|
<a href="#fahrzeuge" data-i18n="navCars">Fahrzeuge</a>
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
<section class="hero" id="home">
|
<section class="hero" id="home">
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<p class="eyebrow" data-i18n="heroEyebrow">MC Cars · Sportwagenvermietung</p>
|
<p class="eyebrow" data-i18n="heroEyebrow">MC Cars · Sportwagenvermietung</p>
|
||||||
<h1 data-i18n="heroTitle">Fahren auf hoechstem Niveau.</h1>
|
<h1 data-i18n="heroTitle">Fahren auf höchstem Niveau.</h1>
|
||||||
<p class="lead" data-i18n="heroLead">Premium-Sportwagen und Luxusklasse in der Steiermark. Kautionsfrei, transparent, sofort startklar.</p>
|
<p class="lead" data-i18n="heroLead">Premium-Sportwagen und Luxusklasse in der Steiermark. Kautionsfrei, transparent, sofort startklar.</p>
|
||||||
|
|
||||||
<div class="hero-cta">
|
<div class="hero-cta">
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<p class="eyebrow" data-i18n="fleetEyebrow">Unsere Flotte</p>
|
<p class="eyebrow" data-i18n="fleetEyebrow">Unsere Flotte</p>
|
||||||
<h2 data-i18n="fleetTitle">Handverlesen. Gepflegt. Startklar.</h2>
|
<h2 data-i18n="fleetTitle">Handverlesen. Gepflegt. Startklar.</h2>
|
||||||
<p class="sub" data-i18n="fleetSub">Filtern Sie nach Marke und Preis. Klicken Sie fuer Details oder buchen Sie direkt.</p>
|
<p class="sub" data-i18n="fleetSub">Filtern Sie nach Marke und Preis. Klicken Sie für Details oder buchen Sie direkt.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow" data-i18n="whyEyebrow">Warum MC Cars</p>
|
<p class="eyebrow" data-i18n="whyEyebrow">Warum MC Cars</p>
|
||||||
<h2 data-i18n="whyTitle">Keine Kompromisse zwischen Sicherheit und Fahrspass.</h2>
|
<h2 data-i18n="whyTitle">Keine Kompromisse zwischen Sicherheit und Fahrspaß.</h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
<article class="why-card">
|
<article class="why-card">
|
||||||
<div class="icon">€</div>
|
<div class="icon">€</div>
|
||||||
<h3 data-i18n="whyDeposit">Faire Kaution</h3>
|
<h3 data-i18n="whyDeposit">Faire Kaution</h3>
|
||||||
<p data-i18n="whyDepositText">Kein Ueberziehen. Transparente, faire Kaution ohne unnoetige Belastung.</p>
|
<p data-i18n="whyDepositText">Kein Überziehen. Transparente, faire Kaution ohne unnötige Belastung.</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
<div class="shell">
|
<div class="shell">
|
||||||
<div class="bpf-header">
|
<div class="bpf-header">
|
||||||
<h2 data-i18n="bpfTitle">Jetzt buchen</h2>
|
<h2 data-i18n="bpfTitle">Jetzt buchen</h2>
|
||||||
<p class="sub" data-i18n="bpfSubtitle">Waehle dein Wunschfahrzeug, den Zeitraum und konfiguriere deine Buchung nach Wunsch.</p>
|
<p class="sub" data-i18n="bpfSubtitle">Wähle dein Wunschfahrzeug, den Zeitraum und konfiguriere deine Buchung nach Wunsch.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step indicators -->
|
<!-- Step indicators -->
|
||||||
@@ -161,7 +161,7 @@
|
|||||||
<div class="bpf-field">
|
<div class="bpf-field">
|
||||||
<label data-i18n="bpfVehicle">Fahrzeug</label>
|
<label data-i18n="bpfVehicle">Fahrzeug</label>
|
||||||
<select id="bpfCar">
|
<select id="bpfCar">
|
||||||
<option value="" data-i18n="bpfSelectVehicle">Fahrzeug waehlen</option>
|
<option value="" data-i18n="bpfSelectVehicle">Fahrzeug wählen</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -191,13 +191,13 @@
|
|||||||
|
|
||||||
<!-- Day mode: single date picker -->
|
<!-- Day mode: single date picker -->
|
||||||
<div class="bpf-field bpf-date-day" id="bpfDateDay" style="display:none;">
|
<div class="bpf-field bpf-date-day" id="bpfDateDay" style="display:none;">
|
||||||
<label data-i18n="bpfPickDate">Datum waehlen</label>
|
<label data-i18n="bpfPickDate">Datum wählen</label>
|
||||||
<input type="date" id="bpfDayDate" />
|
<input type="date" id="bpfDayDate" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Weekend mode: pick the Saturday -->
|
<!-- Weekend mode: pick the Saturday -->
|
||||||
<div class="bpf-field bpf-date-weekend" id="bpfDateWeekend" style="display:none;">
|
<div class="bpf-field bpf-date-weekend" id="bpfDateWeekend" style="display:none;">
|
||||||
<label data-i18n="bpfPickWeekend">Wochenende waehlen (Samstag)</label>
|
<label data-i18n="bpfPickWeekend">Wochenende wählen (Samstag)</label>
|
||||||
<input type="date" id="bpfWeekendDate" />
|
<input type="date" id="bpfWeekendDate" />
|
||||||
<p class="bpf-weekend-def" data-i18n="bpfWeekendDef">Wochenende: Samstag 9:00 – Sonntag 20:00</p>
|
<p class="bpf-weekend-def" data-i18n="bpfWeekendDef">Wochenende: Samstag 9:00 – Sonntag 20:00</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,11 +242,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="bpf-field">
|
<div class="bpf-field">
|
||||||
<label data-i18n="fieldMessage">Nachricht</label>
|
<label data-i18n="fieldMessage">Nachricht</label>
|
||||||
<textarea id="bpfMessage" rows="3" data-i18n-placeholder="messagePlaceholder" placeholder="Wuensche, Uhrzeit, Anlass..."></textarea>
|
<textarea id="bpfMessage" rows="3" data-i18n-placeholder="messagePlaceholder" placeholder="Wünsche, Uhrzeit, Anlass..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bpf-nav">
|
<div class="bpf-nav">
|
||||||
<button class="btn ghost" type="button" id="bpfBack2" data-i18n="bpfBack">Zurueck</button>
|
<button class="btn ghost" type="button" id="bpfBack2" data-i18n="bpfBack">Zurück</button>
|
||||||
<button class="btn" type="button" id="bpfNext2" data-i18n="bpfNext">Weiter</button>
|
<button class="btn" type="button" id="bpfNext2" data-i18n="bpfNext">Weiter</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -254,10 +254,10 @@
|
|||||||
<!-- Step 3: ID Verification -->
|
<!-- Step 3: ID Verification -->
|
||||||
<div class="bpf-panel" id="bpfStep3" style="display:none;">
|
<div class="bpf-panel" id="bpfStep3" style="display:none;">
|
||||||
<h3 class="bpf-panel-title">🔐 <span data-i18n="stepVerification">ID-Verifizierung</span></h3>
|
<h3 class="bpf-panel-title">🔐 <span data-i18n="stepVerification">ID-Verifizierung</span></h3>
|
||||||
<p class="muted" style="margin-bottom:1.5rem;">Bitte laden Sie einen gueltigen Ausweis sowie einen aktuellen Lohnzettel / Gehaltsnachweis hoch.</p>
|
<p class="muted" style="margin-bottom:1.5rem;">Bitte laden Sie einen gültigen Ausweis sowie einen aktuellen Lohnzettel / Gehaltsnachweis hoch.</p>
|
||||||
|
|
||||||
<div class="bpf-field">
|
<div class="bpf-field">
|
||||||
<label data-i18n="bpfIdUpload">Ausweis / Fuehrerschein *</label>
|
<label data-i18n="bpfIdUpload">Ausweis / Führerschein *</label>
|
||||||
<div class="bpf-upload-box" id="uploadId">
|
<div class="bpf-upload-box" id="uploadId">
|
||||||
<span class="bpf-upload-icon">⬆</span>
|
<span class="bpf-upload-icon">⬆</span>
|
||||||
<span data-i18n="bpfClickUpload">Klicken zum Hochladen</span>
|
<span data-i18n="bpfClickUpload">Klicken zum Hochladen</span>
|
||||||
@@ -268,7 +268,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bpf-field">
|
<div class="bpf-field">
|
||||||
<label data-i18n="bpfIncomeUpload">Lohnzettel / Gehaltsnachweis *</label>
|
<label data-i18n="bpfIncomeUpload">Lohnzettel / Gehaltsnachweis (optional)</label>
|
||||||
<div class="bpf-upload-box" id="uploadIncome">
|
<div class="bpf-upload-box" id="uploadIncome">
|
||||||
<span class="bpf-upload-icon">⬆</span>
|
<span class="bpf-upload-icon">⬆</span>
|
||||||
<span data-i18n="bpfClickUpload">Klicken zum Hochladen</span>
|
<span data-i18n="bpfClickUpload">Klicken zum Hochladen</span>
|
||||||
@@ -280,11 +280,11 @@
|
|||||||
|
|
||||||
<div class="bpf-notice">
|
<div class="bpf-notice">
|
||||||
<span>ℹ</span>
|
<span>ℹ</span>
|
||||||
<p data-i18n="bpfIdNotice">Ihre Dokumente werden vertraulich behandelt und dienen ausschliesslich der Identitaetsverifizierung.</p>
|
<p data-i18n="bpfIdNotice">Ihre Dokumente werden vertraulich behandelt und dienen ausschließlich der Identitätsverifizierung.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bpf-nav">
|
<div class="bpf-nav">
|
||||||
<button class="btn ghost" type="button" id="bpfBack3" data-i18n="bpfBack">Zurueck</button>
|
<button class="btn ghost" type="button" id="bpfBack3" data-i18n="bpfBack">Zurück</button>
|
||||||
<button class="btn" type="button" id="bpfSubmit" data-i18n="bpfSubmit">Anfrage absenden</button>
|
<button class="btn" type="button" id="bpfSubmit" data-i18n="bpfSubmit">Anfrage absenden</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,7 +292,7 @@
|
|||||||
|
|
||||||
<!-- Price sidebar -->
|
<!-- Price sidebar -->
|
||||||
<aside class="bpf-sidebar" id="bpfSidebar">
|
<aside class="bpf-sidebar" id="bpfSidebar">
|
||||||
<p class="bpf-sidebar-placeholder" data-i18n="bpfSelectForPrice">Waehle Fahrzeug und Datum fuer eine Preisuebersicht</p>
|
<p class="bpf-sidebar-placeholder" data-i18n="bpfSelectForPrice">Wähle Fahrzeug und Datum für eine Preisübersicht</p>
|
||||||
<div class="bpf-sidebar-content" id="bpfSidebarContent" style="display:none;"></div>
|
<div class="bpf-sidebar-content" id="bpfSidebarContent" style="display:none;"></div>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
<span class="logo-mark">MC</span>
|
<span class="logo-mark">MC</span>
|
||||||
<span>MC Cars</span>
|
<span>MC Cars</span>
|
||||||
</div>
|
</div>
|
||||||
<p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in Oesterreich. Standort: Steiermark (TBD).</p>
|
<p style="color:var(--muted);font-size:0.9rem;max-width:40ch;" data-i18n="footerTagline">Sportwagenvermietung in Österreich. Standort: Steiermark (TBD).</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- MC Cars - Public lead creation RPC
|
||||||
|
-- Allows anon/authenticated callers to create leads and receive the inserted id
|
||||||
|
-- without granting SELECT on public.leads.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
create or replace function public.create_lead(
|
||||||
|
p_name text,
|
||||||
|
p_email text,
|
||||||
|
p_phone text default '',
|
||||||
|
p_vehicle_id uuid default null,
|
||||||
|
p_vehicle_label text default '',
|
||||||
|
p_date_from date default null,
|
||||||
|
p_date_to date default null,
|
||||||
|
p_message text default '',
|
||||||
|
p_source text default 'website'
|
||||||
|
)
|
||||||
|
returns uuid
|
||||||
|
language plpgsql
|
||||||
|
security definer
|
||||||
|
set search_path = public
|
||||||
|
as $$
|
||||||
|
declare
|
||||||
|
v_id uuid;
|
||||||
|
begin
|
||||||
|
insert into public.leads (
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
vehicle_id,
|
||||||
|
vehicle_label,
|
||||||
|
date_from,
|
||||||
|
date_to,
|
||||||
|
message,
|
||||||
|
source
|
||||||
|
)
|
||||||
|
values (
|
||||||
|
p_name,
|
||||||
|
p_email,
|
||||||
|
coalesce(p_phone, ''),
|
||||||
|
p_vehicle_id,
|
||||||
|
coalesce(p_vehicle_label, ''),
|
||||||
|
p_date_from,
|
||||||
|
p_date_to,
|
||||||
|
coalesce(p_message, ''),
|
||||||
|
coalesce(p_source, 'website')
|
||||||
|
)
|
||||||
|
returning id into v_id;
|
||||||
|
|
||||||
|
return v_id;
|
||||||
|
end;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
revoke all on function public.create_lead(text, text, text, uuid, text, date, date, text, text) from public;
|
||||||
|
grant execute on function public.create_lead(text, text, text, uuid, text, date, date, text, text) to anon, authenticated, service_role;
|
||||||
|
|
||||||
|
notify pgrst, 'reload schema';
|
||||||
Reference in New Issue
Block a user