nav.js

4.0 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// Suppress the dense static dots before the timeline markup can paint.6document.documentElement.classList.add("timeline-pending");7// A failed deferred script never reaches the timeline's finally block.8window.addEventListener("load", () => {9	if (typeof window.timeline !== "function") {10		document.documentElement.classList.remove("timeline-pending");11	}12});1314{15	const token = /(?:^|;\s*)kepr_editor=([0-9a-f]{64})(?:;|$)/.exec(document.cookie)?.[1];16	const link = document.querySelector('.nav-links a[href^="/sign-in"]');17	if (token && link) {18		const admin = document.createElement("a");19		admin.href = "/admin";20		admin.textContent = "admin";21		const form = document.createElement("form");22		form.className = "nav-logout";23		form.method = "post";24		form.action = "/admin/sign-out";25		const field = document.createElement("input");26		field.type = "hidden";27		field.name = "form";28		field.value = token;29		const button = document.createElement("button");30		button.type = "submit";31		button.textContent = "log out";32		form.append(field, button);33		const bell = document.createElement("details");34		bell.className = "nav-notifications";35		const toggle = document.createElement("summary");36		toggle.setAttribute("aria-label", "notifications");37		toggle.innerHTML =38			'<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>';39		const dot = document.createElement("span");40		dot.className = "notification-dot";41		dot.hidden = true;42		toggle.append(dot);43		const preview = document.createElement("div");44		preview.className = "notification-preview";45		const heading = document.createElement("strong");46		heading.textContent = "notifications";47		const list = document.createElement("div");48		list.className = "notification-list";49		list.textContent = "loading…";50		const all = document.createElement("a");51		all.href = "/admin/notifications";52		all.className = "notification-all";53		all.textContent = "view all notifications ↗";54		preview.append(heading, list, all);55		bell.append(toggle, preview);56		link.parentElement.after(bell);57		let loading = false;58		const refresh = async () => {59			if (loading) return;60			loading = true;61			try {62				const response = await fetch("/admin/notifications?format=json", { cache: "no-store" });63				if (response.status === 401) {64					bell.remove();65					return;66				}67				if (!response.ok) throw new Error(`notifications: ${response.status}`);68				const feed = await response.json();69				dot.hidden = feed.unread === 0;70				toggle.setAttribute("aria-label", `notifications, ${feed.unread} unread`);71				list.replaceChildren();72				for (const item of feed.notifications) {73					const entry = document.createElement("a");74					entry.href = `/admin/notifications/${item.id}`;75					entry.className = item.read ? "" : "notification-unread";76					const text = document.createElement("span");77					text.textContent = item.text;78					const when = document.createElement("small");79					when.textContent = `${item.read ? "" : "unread · "}${item.when}`;80					entry.append(text, when);81					list.append(entry);82				}83				if (!feed.notifications.length) list.textContent = "nothing yet";84			} catch (error) {85				list.textContent = "Could not load notifications. Open the full list or try again.";86				console.error(error);87			} finally {88				loading = false;89			}90		};91		bell.addEventListener("toggle", () => {92			if (bell.open) refresh();93		});94		document.addEventListener("click", (event) => {95			if (!bell.contains(event.target)) bell.open = false;96		});97		document.addEventListener("keydown", (event) => {98			if (event.key === "Escape" && bell.open) {99				bell.open = false;100				toggle.focus();101			}102		});103		refresh();104105		link.replaceWith(admin, form);106	}107}