glo/static/js/06-pages.js
2026-11-02 04:31:52 +00:00

426 lines
12 KiB
JavaScript

// 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 || "<p>no content yet.</p>";
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");
}
};