466 lines
12 KiB
JavaScript
466 lines
12 KiB
JavaScript
// static/js/06-pages.js
|
|
"use strict";
|
|
|
|
window.Glow = window.Glow || {};
|
|
|
|
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";
|
|
|
|
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";
|
|
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 excerpt = document.createElement("p");
|
|
excerpt.className = "page-card-excerpt";
|
|
|
|
const compactBody = (page.body || "").replace(/\s+/g, " ").trim();
|
|
|
|
excerpt.textContent =
|
|
compactBody.length > 180
|
|
? `${compactBody.slice(0, 180)}…`
|
|
: compactBody || "No content yet.";
|
|
|
|
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, excerpt, 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);
|
|
|
|
if (page.can_edit) {
|
|
const actions = document.createElement("div");
|
|
actions.className = "page-detail-actions";
|
|
|
|
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");
|
|
}
|
|
};
|