nav.js

3.7 KB · raw

1// The navbar when signed in: "admin" and "log out" in place of "log in". Loaded right after the2// nav, before the rest of the page, so the signed-out links never show first. Pages are the same3// for everyone; the readable kepr_editor cookie, set beside the session, carries the form token4// that log out posts back. Only the private notification preview needs a request.5{6	const token = /(?:^|;\s*)kepr_editor=([0-9a-f]{64})(?:;|$)/.exec(document.cookie)?.[1];7	const link = document.querySelector('.nav-links a[href^="/sign-in"]');8	if (token && link) {9		const admin = document.createElement("a");10		admin.href = "/admin";11		admin.textContent = "admin";12		const form = document.createElement("form");13		form.className = "nav-logout";14		form.method = "post";15		form.action = "/admin/sign-out";16		const field = document.createElement("input");17		field.type = "hidden";18		field.name = "form";19		field.value = token;20		const button = document.createElement("button");21		button.type = "submit";22		button.textContent = "log out";23		form.append(field, button);24		const bell = document.createElement("details");25		bell.className = "nav-notifications";26		const toggle = document.createElement("summary");27		toggle.setAttribute("aria-label", "notifications");28		toggle.innerHTML =29			'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></svg>';30		const dot = document.createElement("span");31		dot.className = "notification-dot";32		dot.hidden = true;33		toggle.append(dot);34		const preview = document.createElement("div");35		preview.className = "notification-preview";36		const heading = document.createElement("strong");37		heading.textContent = "notifications";38		const list = document.createElement("div");39		list.className = "notification-list";40		list.textContent = "loading…";41		const all = document.createElement("a");42		all.href = "/admin/notifications";43		all.className = "notification-all";44		all.textContent = "view all notifications ↗";45		preview.append(heading, list, all);46		bell.append(toggle, preview);47		link.parentElement.after(bell);48		let loading = false;49		const refresh = async () => {50			if (loading) return;51			loading = true;52			try {53				const response = await fetch("/admin/notifications?format=json", { cache: "no-store" });54				if (response.status === 401) {55					bell.remove();56					return;57				}58				if (!response.ok) throw new Error(`notifications: ${response.status}`);59				const feed = await response.json();60				dot.hidden = feed.unread === 0;61				toggle.setAttribute("aria-label", `notifications, ${feed.unread} unread`);62				list.replaceChildren();63				for (const item of feed.notifications) {64					const entry = document.createElement("a");65					entry.href = `/admin/notifications/${item.id}`;66					entry.className = item.read ? "" : "notification-unread";67					const text = document.createElement("span");68					text.textContent = item.text;69					const when = document.createElement("small");70					when.textContent = `${item.read ? "" : "unread · "}${item.when}`;71					entry.append(text, when);72					list.append(entry);73				}74				if (!feed.notifications.length) list.textContent = "nothing yet";75			} catch (error) {76				list.textContent = "Could not load notifications. Open the full list or try again.";77				console.error(error);78			} finally {79				loading = false;80			}81		};82		bell.addEventListener("toggle", () => {83			if (bell.open) refresh();84		});85		document.addEventListener("click", (event) => {86			if (!bell.contains(event.target)) bell.open = false;87		});88		document.addEventListener("keydown", (event) => {89			if (event.key === "Escape" && bell.open) {90				bell.open = false;91				toggle.focus();92			}93		});94		refresh();9596		link.replaceWith(admin, form);97	}98}