// static/js/06-pages.js "use strict"; window.Glow = window.Glow || {}; // comments, version history, and share links intentionally disabled in production until backend support complete Glow.renderPagesEmptyState = message => { Glow.els.pagesBox.replaceChildren(); const state = document.createElement("div"); state.className = "empty-state"; const card = document.createElement("div"); card.className = "empty-card"; card.textContent = message; state.append(card); Glow.els.pagesBox.append(state); }; Glow.pageTypeLabel = type => { const labels = { project: "project", document: "document", update: "update", resource: "resource" }; return labels[type] || "page"; }; Glow.loadPages = async (pageType = Glow.state.pageTypeFilter) => { const team = Glow.state.activeTeam; if (!team) { Glow.renderPagesEmptyState("select a team to view pages."); return; } Glow.els.pagesBox.setAttribute("aria-busy", "true"); try { const query = pageType ? `?type=${encodeURIComponent(pageType)}` : ""; const data = await Glow.apiRequest( `/api/teams/${encodeURIComponent(team.slug)}/pages${query}` ); if (Glow.state.activeTeam?.slug !== team.slug) return; Glow.state.pages = data.pages || []; Glow.state.pageTypeFilter = pageType || ""; Glow.renderPages(); } catch (error) { Glow.renderPagesEmptyState(error.message || "could not load pages."); Glow.showToast(error.message || "could not load pages.", "error"); } finally { Glow.els.pagesBox.setAttribute("aria-busy", "false"); } }; Glow.renderPages = () => { const box = Glow.els.pagesBox; const team = Glow.state.activeTeam; const pages = Glow.state.pages || []; box.replaceChildren(); const header = document.createElement("div"); header.className = "pages-header"; const copy = document.createElement("div"); const title = document.createElement("h2"); title.textContent = `${team?.name || "team"} pages`; const subtitle = document.createElement("p"); subtitle.textContent = "shared team knowledge and public resources."; copy.append(title, subtitle); header.append(copy); if (team?.can_manage) { const create = document.createElement("button"); create.className = "button"; create.type = "button"; create.textContent = "new page"; create.addEventListener("click", Glow.createPage); header.append(create); } box.append(header); const filters = document.createElement("div"); filters.className = "page-filters compact"; const filterOptions = [ ["", "all"], ["project", "projects"], ["document", "documents"], ["update", "updates"], ["resource", "resources"] ]; for (const [value, label] of filterOptions) { const button = document.createElement("button"); button.className = "page-filter"; button.type = "button"; button.textContent = label; button.classList.toggle("active", Glow.state.pageTypeFilter === value); button.addEventListener("click", () => Glow.loadPages(value)); filters.append(button); } box.append(filters); if (!pages.length) { const empty = document.createElement("div"); empty.className = "empty-state page-empty-state"; const card = document.createElement("div"); card.className = "empty-card"; card.textContent = "no pages match this filter."; empty.append(card); box.append(empty); return; } const list = document.createElement("div"); list.className = "pages-list"; for (const page of pages) { list.append(Glow.buildPageCard(page)); } box.append(list); }; Glow.buildPageCard = page => { const article = document.createElement("article"); article.className = "page-card compact"; article.tabIndex = 0; const meta = document.createElement("div"); meta.className = "page-card-meta"; const type = document.createElement("span"); type.className = "page-badge type"; type.textContent = Glow.pageTypeLabel(page.page_type); const visibility = document.createElement("span"); visibility.className = `page-badge visibility ${page.visibility}`; visibility.textContent = page.visibility; meta.append(type, visibility); const title = document.createElement("h3"); title.textContent = page.title; const footer = document.createElement("div"); footer.className = "page-card-footer"; const context = document.createElement("span"); context.textContent = page.channel_name ? `#${page.channel_name}` : "team page"; const updated = document.createElement("time"); updated.dateTime = page.updated_at; updated.title = new Date(page.updated_at).toLocaleString(); updated.textContent = `updated ${Glow.relativeTime(page.updated_at)}`; footer.append(context, updated); const open = () => Glow.openPage(page.slug); article.addEventListener("click", open); article.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); open(); } }); article.append(meta, title, footer); return article; }; Glow.openPages = async () => { Glow.state.activeDm = null; Glow.state.activeView = "pages"; Glow.state.activePage = null; Glow.updateViewUI(); await Glow.loadPages(); }; Glow.openPage = async pageSlug => { const team = Glow.state.activeTeam; if (!team) return; Glow.state.activeView = "page"; Glow.state.activePage = null; Glow.updateViewUI(); Glow.els.pageDetailBox.setAttribute("aria-busy", "true"); try { const data = await Glow.apiRequest( `/api/teams/${encodeURIComponent(team.slug)}/pages/${encodeURIComponent(pageSlug)}` ); Glow.state.activePage = data.page; Glow.renderPageDetail(); } catch (error) { Glow.showToast(error.message || "could not load page.", "error"); Glow.openPages(); } finally { Glow.els.pageDetailBox.setAttribute("aria-busy", "false"); } }; Glow.renderPageDetail = () => { const box = Glow.els.pageDetailBox; const page = Glow.state.activePage; box.replaceChildren(); if (!page) { const state = document.createElement("div"); state.className = "empty-state"; const card = document.createElement("div"); card.className = "empty-card"; card.textContent = "page not found."; state.append(card); box.append(state); return; } const header = document.createElement("div"); header.className = "page-detail-header"; const meta = document.createElement("div"); meta.className = "page-detail-meta"; const type = document.createElement("span"); type.className = "page-badge type"; type.textContent = Glow.pageTypeLabel(page.page_type); const visibility = document.createElement("span"); visibility.className = `page-badge visibility ${page.visibility}`; visibility.textContent = page.visibility; const updated = document.createElement("time"); updated.className = "page-updated"; updated.dateTime = page.updated_at; updated.title = new Date(page.updated_at).toLocaleString(); updated.textContent = `updated ${Glow.relativeTime(page.updated_at)}`; meta.append(type, visibility, updated); const title = document.createElement("h1"); title.textContent = page.title; const context = document.createElement("p"); context.className = "page-detail-context"; context.textContent = page.channel_name ? `${page.team_name} ยท #${page.channel_name}` : page.team_name; header.append(meta, title, context); box.append(header); const body = document.createElement("article"); body.className = "page-body markdown-body"; body.innerHTML = page.body_html || "
no content yet.
"; box.append(body); const actions = document.createElement("div"); actions.className = "page-detail-actions"; if (page.can_edit) { const edit = document.createElement("button"); edit.className = "button"; edit.type = "button"; edit.textContent = "edit"; edit.addEventListener("click", () => Glow.editPage(page)); const remove = document.createElement("button"); remove.className = "danger-button"; remove.type = "button"; remove.textContent = "delete"; remove.addEventListener("click", () => Glow.deletePage(page)); actions.append(edit, remove); } box.append(actions); }; Glow.pageDialogFields = (page = null) => [ { name: "title", label: "title", value: page?.title || "", placeholder: "page title", maxLength: 160 }, { name: "page_type", label: "type: project, document, update, or resource", value: page?.page_type || "document", maxLength: 12 }, { name: "visibility", label: "visibility: team or public", value: page?.visibility || "team", maxLength: 6 }, { name: "channel_slug", label: "channel slug (optional)", value: page?.channel_slug || "", placeholder: "general", maxLength: 48 }, { name: "body", label: "markdown content", value: page?.body || "", multiline: true, rows: 14, maxLength: 100000 } ]; Glow.validatePageValues = values => { if (!values.title) { Glow.showToast("page title is required.", "error"); return null; } const pageType = values.page_type.toLowerCase(); if (!["project", "document", "update", "resource"].includes(pageType)) { Glow.showToast("choose a valid page type.", "error"); return null; } const visibility = values.visibility.toLowerCase(); if (!["team", "public"].includes(visibility)) { Glow.showToast("visibility must be team or public.", "error"); return null; } return { title: values.title, body: values.body, page_type: pageType, visibility, channel_slug: values.channel_slug.toLowerCase() }; }; Glow.createPage = async () => { const team = Glow.state.activeTeam; if (!team?.can_manage) return; const values = await Glow.openDialog({ title: "create page", body: "create a shared team page or a public resource. markdown is supported.", confirmText: "create page", fields: Glow.pageDialogFields() }); if (!values) return; const payload = Glow.validatePageValues(values); if (!payload) return; try { const data = await Glow.apiRequest( `/api/teams/${encodeURIComponent(team.slug)}/pages`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) } ); Glow.showToast("page created."); await Glow.openPage(data.page.slug); } catch (error) { Glow.showToast(error.message || "could not create page.", "error"); } }; Glow.editPage = async page => { if (!page.can_edit) return; const values = await Glow.openDialog({ title: `edit ${page.title}`, confirmText: "save", fields: Glow.pageDialogFields(page) }); if (!values) return; const payload = Glow.validatePageValues(values); if (!payload) return; try { const data = await Glow.apiRequest( `/api/teams/${encodeURIComponent(page.team_slug)}/pages/${encodeURIComponent(page.slug)}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) } ); Glow.state.activePage = data.page; Glow.renderPageDetail(); Glow.showToast("page updated."); } catch (error) { Glow.showToast(error.message || "could not update page.", "error"); } }; Glow.deletePage = async page => { if (!page.can_edit) return; const result = await Glow.openDialog({ title: `delete ${page.title}?`, body: "this page will no longer be available.", confirmText: "delete page", danger: true }); if (!result) return; try { await Glow.apiRequest( `/api/teams/${encodeURIComponent(page.team_slug)}/pages/${encodeURIComponent(page.slug)}`, { method: "DELETE" } ); Glow.showToast("page deleted."); await Glow.openPages(); } catch (error) { Glow.showToast(error.message || "could not delete page.", "error"); } };