107 lines
4.0 KiB
TypeScript
107 lines
4.0 KiB
TypeScript
/**
|
|
* <folk-schedule-admin> — admin dashboard for rSchedule.
|
|
*
|
|
* Passkey-gated (server enforces moderator role on every admin API call).
|
|
* Phase A: minimal shell with tab placeholders. Phase E builds full UI
|
|
* (availability rules, overrides, bookings list, settings, gcal connect).
|
|
*/
|
|
|
|
class FolkScheduleAdmin extends HTMLElement {
|
|
private shadow: ShadowRoot;
|
|
private space = "";
|
|
private tab: "overview" | "availability" | "bookings" | "calendar" | "settings" = "overview";
|
|
private err = "";
|
|
|
|
constructor() {
|
|
super();
|
|
this.shadow = this.attachShadow({ mode: "open" });
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.space = this.getAttribute("space") || "demo";
|
|
this.render();
|
|
}
|
|
|
|
private authHeaders(): Record<string, string> {
|
|
try {
|
|
const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}");
|
|
const token = sess?.token;
|
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
} catch { return {}; }
|
|
}
|
|
|
|
private async ping() {
|
|
const path = window.location.pathname;
|
|
const m = path.match(/^(\/[^/]+)?\/rschedule/);
|
|
const base = `${m?.[0] || "/rschedule"}/api/admin`;
|
|
const res = await fetch(`${base}/settings`, { headers: this.authHeaders() });
|
|
if (res.status === 401) { this.err = "Sign in to manage this booking page."; return; }
|
|
if (res.status === 403) { this.err = "You need moderator or higher role in this space."; return; }
|
|
if (!res.ok) { this.err = `Error ${res.status}`; return; }
|
|
this.err = "";
|
|
}
|
|
|
|
private render() {
|
|
void this.ping().then(() => this.paint());
|
|
this.paint();
|
|
}
|
|
|
|
private paint() {
|
|
this.shadow.innerHTML = `
|
|
<style>
|
|
:host { display:block; color:#e2e8f0; font-family:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background:#0b1120; min-height:100vh; }
|
|
.wrap { max-width:1100px; margin:0 auto; padding:32px 24px; }
|
|
h1 { margin:0 0 6px; font-size:1.4rem; }
|
|
.sub { color:#94a3b8; margin:0 0 24px; font-size:0.9rem; }
|
|
.tabs { display:flex; gap:4px; border-bottom:1px solid rgba(148,163,184,0.12); margin-bottom:24px; overflow-x:auto; }
|
|
.tab { background:transparent; color:#94a3b8; border:0; padding:10px 16px; cursor:pointer; font-size:0.9rem; border-bottom:2px solid transparent; }
|
|
.tab.active { color:#06b6d4; border-bottom-color:#06b6d4; }
|
|
.card { background:#111827; border:1px solid rgba(148,163,184,0.12); border-radius:12px; padding:24px; }
|
|
.err { padding:16px; border:1px solid rgba(239,68,68,0.3); background:rgba(239,68,68,0.08); border-radius:8px; color:#fca5a5; margin-bottom:16px; }
|
|
.placeholder { color:#94a3b8; padding:40px; text-align:center; border:1px dashed rgba(148,163,184,0.25); border-radius:10px; }
|
|
</style>
|
|
<div class="wrap">
|
|
<h1>rSchedule admin · ${escapeHtml(this.space)}</h1>
|
|
<p class="sub">Configure availability, bookings, and Google Calendar for this space.</p>
|
|
${this.err ? `<div class="err">${escapeHtml(this.err)}</div>` : ""}
|
|
<div class="tabs">
|
|
${this.tabBtn("overview", "Overview")}
|
|
${this.tabBtn("availability", "Availability")}
|
|
${this.tabBtn("bookings", "Bookings")}
|
|
${this.tabBtn("calendar", "Google Calendar")}
|
|
${this.tabBtn("settings", "Settings")}
|
|
</div>
|
|
<div class="card">
|
|
<div class="placeholder">
|
|
<strong>${escapeHtml(this.tab)}</strong><br>
|
|
Full admin UI arrives in Phase E. For now, this confirms auth gate works.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
this.shadow.querySelectorAll<HTMLButtonElement>(".tab").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
this.tab = btn.dataset.tab as any;
|
|
this.paint();
|
|
});
|
|
});
|
|
}
|
|
|
|
private tabBtn(id: string, label: string): string {
|
|
return `<button class="tab ${this.tab === id ? "active" : ""}" data-tab="${id}">${label}</button>`;
|
|
}
|
|
}
|
|
|
|
function escapeHtml(s: string): string {
|
|
return String(s)
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
customElements.define("folk-schedule-admin", FolkScheduleAdmin);
|
|
|
|
export {};
|