Ports inventory management and SCA-style cupping scoring from hope_roaster: lot tracking with audit-logged consumption, cupping sessions with server-authoritative scoring and a live radar chart, and links from the planner (draw-from-lot, open-cupping-session). Also fixes the Blend/Single-origin toggle layout, replaces tooltips with an in-context "why" teaching layer, and stages the planner UI into pre-roast vs. post-roast phases.
63 lines
2.3 KiB
JavaScript
63 lines
2.3 KiB
JavaScript
// Shared side-nav behavior (collapse/expand, mobile drawer) for every authenticated page
|
|
// (planner, account, admin) so each page's own script doesn't reimplement it.
|
|
export function initSideNav() {
|
|
const nav = document.getElementById("side-nav");
|
|
const hamburger = document.getElementById("nav-hamburger");
|
|
const collapseBtn = document.getElementById("nav-collapse");
|
|
const workspace = document.getElementById("app-workspace");
|
|
const overlay = document.getElementById("drawer-overlay");
|
|
const COLLAPSE_KEY = "roastPlannerNavCollapsed";
|
|
|
|
function closeMobileNav() {
|
|
nav.classList.remove("mobile-open");
|
|
hamburger?.setAttribute("aria-expanded", "false");
|
|
if (!document.querySelector(".drawer:not(.hidden)"))
|
|
overlay?.classList.add("hidden");
|
|
}
|
|
hamburger?.addEventListener("click", () => {
|
|
const open = nav.classList.toggle("mobile-open");
|
|
hamburger.setAttribute("aria-expanded", String(open));
|
|
overlay?.classList.toggle("hidden", !open);
|
|
});
|
|
overlay?.addEventListener("click", closeMobileNav);
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key === "Escape") closeMobileNav();
|
|
});
|
|
|
|
function applyCollapsed(collapsed) {
|
|
nav.classList.toggle("collapsed", collapsed);
|
|
workspace?.classList.toggle("nav-collapsed", collapsed);
|
|
if (collapseBtn) {
|
|
collapseBtn.textContent = collapsed ? "»" : "«";
|
|
collapseBtn.setAttribute(
|
|
"aria-label",
|
|
collapsed ? "Expand navigation" : "Collapse navigation",
|
|
);
|
|
}
|
|
}
|
|
applyCollapsed(localStorage.getItem(COLLAPSE_KEY) === "true");
|
|
collapseBtn?.addEventListener("click", () => {
|
|
const next = !nav.classList.contains("collapsed");
|
|
applyCollapsed(next);
|
|
localStorage.setItem(COLLAPSE_KEY, String(next));
|
|
});
|
|
return { closeMobileNav };
|
|
}
|
|
|
|
/** Populates the nav's user chip and admin link; redirects to /login when signed out. */
|
|
export async function loadNavUser() {
|
|
const response = await fetch("/api/auth/me");
|
|
if (!response.ok) {
|
|
location.replace("/login");
|
|
return null;
|
|
}
|
|
const { user } = await response.json();
|
|
const emailEl = document.getElementById("account-email");
|
|
if (emailEl) emailEl.textContent = user.email;
|
|
const avatarEl = document.getElementById("nav-user-avatar");
|
|
if (avatarEl) avatarEl.textContent = user.email.charAt(0).toUpperCase();
|
|
if (user.role === "admin")
|
|
document.getElementById("nav-admin")?.classList.remove("hidden");
|
|
return user;
|
|
}
|