fix(hugo): Layout hosting→appshell umbenannt (Section-Kollision)
layouts/hosting.html kollidierte mit der content/hosting/-Section → Hugo verlangte ein section-Template und der Build brach ab (kein public/). Layout heisst jetzt 'appshell', login/register/konto/hosting-preise referenzieren es. Build wieder grün, alle Seiten + Tab-CSS verifiziert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+125
-27
@@ -105,51 +105,149 @@
|
||||
};
|
||||
}
|
||||
|
||||
// ── Konto / Dashboard ──────────────────────────────────────────────────
|
||||
// ── Konto / Dashboard (Tabs: Übersicht · Profil · Sicherheit) ───────────
|
||||
let acctData = null; // gecachte /account/me-Antwort
|
||||
let acctTab = "overview";
|
||||
|
||||
async function renderKonto() {
|
||||
if (!tok.isLoggedIn) return go("/login/");
|
||||
root.innerHTML = card('<div class="hosting-sub">Lädt…</div>', true);
|
||||
let data;
|
||||
try { data = await api("GET", "/account/me"); }
|
||||
try { acctData = await api("GET", "/account/me"); }
|
||||
catch (err) {
|
||||
if (/angemeldet|abgelaufen|ungültig/i.test(err.message)) { tok.clear(); return go("/login/"); }
|
||||
root.innerHTML = card('<div class="hosting-msg err">' + esc(err.message) + "</div>"); return;
|
||||
}
|
||||
const { account, subscription, instance, plans } = data;
|
||||
const params = new URLSearchParams(location.search);
|
||||
const justOk = params.get("provisioned") === "1";
|
||||
paintKonto();
|
||||
}
|
||||
|
||||
let html =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:24px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Konto</div>' +
|
||||
function paintKonto() {
|
||||
const { account } = acctData;
|
||||
const tabs = [["overview", "Übersicht"], ["profile", "Profil"], ["security", "Sicherheit"]];
|
||||
const head =
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">' +
|
||||
'<div class="hosting-title" style="text-align:left;margin:0">Mein Konto</div>' +
|
||||
'<button class="hosting-link" id="logout">Abmelden</button></div>' +
|
||||
'<div class="hosting-sub" style="text-align:left;margin-bottom:20px">' + esc(account.email) + "</div>";
|
||||
'<div class="hosting-sub" style="text-align:left;margin-bottom:20px">' + esc(account.email) + "</div>" +
|
||||
'<div class="hosting-tabs">' +
|
||||
tabs.map(([id, label]) =>
|
||||
'<button class="hosting-tab' + (acctTab === id ? " active" : "") + '" data-tab="' + id + '">' + label + "</button>"
|
||||
).join("") + "</div>";
|
||||
|
||||
if (justOk) html += '<div class="hosting-msg ok">Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.</div>';
|
||||
const body = { overview: tabOverview, profile: tabProfile, security: tabSecurity }[acctTab]();
|
||||
root.innerHTML = card(head + '<div id="tabbody">' + body + "</div>", true);
|
||||
|
||||
root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); };
|
||||
root.querySelectorAll("[data-tab]").forEach((b) =>
|
||||
(b.onclick = () => { acctTab = b.dataset.tab; paintKonto(); })
|
||||
);
|
||||
wireTab();
|
||||
}
|
||||
|
||||
// — Tab: Übersicht (Abo + Instanzen) —
|
||||
function tabOverview() {
|
||||
const { subscription, instances, plans } = acctData;
|
||||
const params = new URLSearchParams(location.search);
|
||||
let h = "";
|
||||
if (params.get("provisioned") === "1")
|
||||
h += '<div class="hosting-msg ok">Zahlung erfolgreich — Ihre Instanz wird bereitgestellt.</div>';
|
||||
|
||||
if (subscription) {
|
||||
html += '<div class="hosting-row"><span class="muted">Abo</span><b>' + esc(subscription.plan) +
|
||||
" · " + esc(subscription.status) + "</b></div>";
|
||||
h += '<div class="hosting-row"><span class="muted">Abo</span><b>' +
|
||||
esc(subscription.plan) + " · " + esc(subscription.status) + "</b></div>";
|
||||
if (subscription.current_period_end)
|
||||
h += '<div class="hosting-row"><span class="muted">Nächste Verlängerung</span><span>' +
|
||||
esc(new Date(subscription.current_period_end).toLocaleDateString("de-CH")) + "</span></div>";
|
||||
}
|
||||
if (instance) {
|
||||
html += '<div class="hosting-row"><span class="muted">Instanz</span><b>' + esc(instance.status) + "</b></div>" +
|
||||
'<a class="hosting-btn hosting-btn-primary" style="display:block;text-align:center;text-decoration:none;margin-top:18px" href="' +
|
||||
esc(instance.instance_url) + '" target="_blank" rel="noreferrer">Rapport öffnen →</a>';
|
||||
|
||||
if (instances && instances.length) {
|
||||
h += '<div style="margin:22px 0 10px;font-weight:600;font-size:14px">Ihre Instanzen</div>';
|
||||
h += instances.map((i) =>
|
||||
'<div class="hosting-row"><div><b>' + esc(i.label || i.studio_slug) + "</b>" +
|
||||
'<div class="muted" style="font-size:12px">' + esc(i.instance_url) + "</div></div>" +
|
||||
'<a class="hosting-btn hosting-btn-primary" style="width:auto;padding:8px 16px;text-decoration:none" href="' +
|
||||
esc(i.instance_url) + '" target="_blank" rel="noreferrer">Öffnen →</a></div>'
|
||||
).join("");
|
||||
// Weitere Instanz (Multi-Instanz vorbereitet — Backend-Sperre öffnen wir später)
|
||||
h += '<div style="margin-top:16px"><button class="hosting-btn hosting-btn-dark" id="addInstance">+ Weitere Instanz</button></div>';
|
||||
} else {
|
||||
html += '<div style="margin:18px 0 8px;font-size:14px;color:var(--rapport-text-2)">Wählen Sie ein Abo, um Ihre Instanz freizuschalten:</div>' +
|
||||
h += '<div style="margin:18px 0 8px;font-size:14px;color:var(--rapport-text-2)">Wählen Sie ein Abo, um Ihre Instanz freizuschalten:</div>' +
|
||||
'<div class="hosting-plans">' + (plans || []).map(planCard).join("") + "</div>";
|
||||
}
|
||||
root.innerHTML = card(html, true);
|
||||
root.querySelector("#logout").onclick = () => { tok.clear(); go("/"); };
|
||||
root.querySelectorAll("[data-plan]").forEach((b) => {
|
||||
b.onclick = async () => {
|
||||
b.disabled = true;
|
||||
return h;
|
||||
}
|
||||
|
||||
// — Tab: Profil (Firma / Rechnungsadresse) —
|
||||
function tabProfile() {
|
||||
const a = acctData.account;
|
||||
const f = (id, label, val, ph) =>
|
||||
'<label class="hosting-label">' + label + "</label>" +
|
||||
'<input class="hosting-input" id="' + id + '" value="' + esc(val || "") + '" placeholder="' + (ph || "") + '">';
|
||||
return '<div id="pmsg"></div>' +
|
||||
f("company", "Firma / Büro", a.company, "Architektur Muster GmbH") +
|
||||
f("contact_name", "Ansprechperson", a.contact_name, "Vor- und Nachname") +
|
||||
f("street", "Strasse & Nr.", a.street) +
|
||||
'<div style="display:flex;gap:12px">' +
|
||||
'<div style="flex:0 0 110px">' + f("zip", "PLZ", a.zip) + "</div>" +
|
||||
'<div style="flex:1">' + f("city", "Ort", a.city) + "</div></div>" +
|
||||
f("country", "Land", a.country || "CH") +
|
||||
f("phone", "Telefon", a.phone) +
|
||||
'<button class="hosting-btn hosting-btn-primary" id="saveProfile" style="margin-top:8px">Speichern</button>';
|
||||
}
|
||||
|
||||
// — Tab: Sicherheit (Passwort) —
|
||||
function tabSecurity() {
|
||||
return '<div id="smsg"></div>' +
|
||||
'<label class="hosting-label">Aktuelles Passwort</label>' +
|
||||
'<input class="hosting-input" type="password" id="curPw">' +
|
||||
'<label class="hosting-label">Neues Passwort</label>' +
|
||||
'<input class="hosting-input" type="password" id="newPw" placeholder="min. 8 Zeichen">' +
|
||||
'<button class="hosting-btn hosting-btn-primary" id="savePw" style="margin-top:8px">Passwort ändern</button>';
|
||||
}
|
||||
|
||||
// — Event-Wiring je Tab —
|
||||
function wireTab() {
|
||||
if (acctTab === "overview") {
|
||||
root.querySelectorAll("[data-plan]").forEach((b) =>
|
||||
(b.onclick = async () => {
|
||||
b.disabled = true;
|
||||
try { const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan }); go(url); }
|
||||
catch (err) { alert(err.message); b.disabled = false; }
|
||||
})
|
||||
);
|
||||
const add = root.querySelector("#addInstance");
|
||||
if (add) add.onclick = () => alert("Weitere Instanzen kommen bald. Kontaktieren Sie uns für ein Mehrfach-Abo.");
|
||||
}
|
||||
if (acctTab === "profile") {
|
||||
root.querySelector("#saveProfile").onclick = async (e) => {
|
||||
const btn = e.target; btn.disabled = true;
|
||||
const m = root.querySelector("#pmsg");
|
||||
const payload = {};
|
||||
["company", "contact_name", "street", "zip", "city", "country", "phone"].forEach(
|
||||
(id) => (payload[id] = root.querySelector("#" + id).value.trim())
|
||||
);
|
||||
try {
|
||||
const { url } = await api("POST", "/billing/checkout", { planId: b.dataset.plan });
|
||||
go(url);
|
||||
} catch (err) { alert(err.message); b.disabled = false; }
|
||||
const { account } = await api("PATCH", "/account/me", payload);
|
||||
acctData.account = { ...acctData.account, ...account };
|
||||
msg(m, "Gespeichert.", "ok");
|
||||
} catch (err) { msg(m, err.message, "err"); }
|
||||
btn.disabled = false;
|
||||
};
|
||||
});
|
||||
}
|
||||
if (acctTab === "security") {
|
||||
root.querySelector("#savePw").onclick = async (e) => {
|
||||
const btn = e.target; btn.disabled = true;
|
||||
const m = root.querySelector("#smsg");
|
||||
try {
|
||||
await api("POST", "/account/password", {
|
||||
currentPassword: root.querySelector("#curPw").value,
|
||||
newPassword: root.querySelector("#newPw").value,
|
||||
});
|
||||
root.querySelector("#curPw").value = ""; root.querySelector("#newPw").value = "";
|
||||
msg(m, "Passwort geändert.", "ok");
|
||||
} catch (err) { msg(m, err.message, "err"); }
|
||||
btn.disabled = false;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function planCard(p) {
|
||||
|
||||
Reference in New Issue
Block a user