add blog page

Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
This commit is contained in:
2026-05-24 09:22:55 +02:00
parent 7cd2dee64c
commit 2b6c7074c2
14 changed files with 407 additions and 22 deletions
@@ -0,0 +1,174 @@
import {capitalize, sendApiGet, TesoftComponent} from "../scripts/main.ts";
import {TesoftLoaderSection} from "./loader-section.ts";
import {TesoftPagination} from "./pagination.ts";
import {TesoftError} from "./error.ts";
import {TesoftTag} from "./tag.ts";
import {formatArticleDate, getBadgeText} from "./blog-article.ts";
import {TesoftBadge} from "./badge.ts";
export class TesoftBlogArticleList extends TesoftComponent {
private readonly listItemTemplate: HTMLTemplateElement;
private readonly loaderSection: TesoftLoaderSection;
private readonly listDiv: HTMLDivElement;
private readonly tagsDiv: HTMLDivElement;
private readonly pagination: TesoftPagination;
private readonly error: TesoftError;
constructor() {
super();
const template = document.getElementById("tesoft-blog-article-list-template") as HTMLTemplateElement;
const templateContent = template.content;
const shadowRoot = this.attachShadow({mode: "open"});
shadowRoot.appendChild(templateContent.cloneNode(true));
this.listItemTemplate = document.getElementById("tesoft-blog-article-list-item-template") as HTMLTemplateElement;
this.loaderSection = shadowRoot.querySelector<TesoftLoaderSection>("tesoft-loader-section")!;
this.listDiv = shadowRoot.querySelector<HTMLDivElement>(".list")!;
this.tagsDiv = shadowRoot.querySelector<HTMLDivElement>(".tags")!;
this.pagination = shadowRoot.querySelector<TesoftPagination>("tesoft-pagination")!;
this.error = shadowRoot.querySelector<TesoftError>("tesoft-error")!;
}
async load(page: number, tags: string[], onParametersChange: (page: number, tags: string[]) => void) {
try {
this.tagsDiv.innerHTML = "";
for (const tagName of await this.fetchTags()) {
const tag = document.createElement("tesoft-tag") as TesoftTag;
tag.textContent = tagName;
this.tagsDiv.appendChild(tag);
}
} catch (error: any) {
// Ignore
}
this.pagination.addEventListener("update-page", (e) => {
const page = (e as CustomEvent).detail.page;
onParametersChange(page, tags);
this.reload(page, tags, onParametersChange);
});
await this.reload(page, tags, onParametersChange);
}
private async reload(page: number, tags: string[], onParametersChange: (page: number, tags: string[]) => void) {
const itemsPerPage = 20;
for (const tag of this.tagsDiv.children as any as TesoftTag[]) {
const tagName = tag.textContent.trim();
tag.onclick = () => {
const tagSet = new Set(tags);
if (tagSet.has(tagName)) {
tagSet.delete(tagName);
} else {
tagSet.add(tagName);
}
const newTags = Array.from(tagSet);
onParametersChange(page, newTags);
this.reload(page, newTags, onParametersChange);
};
if (tags.includes(tagName)) {
tag.selected = "";
} else {
tag.selected = null;
}
}
this.loaderSection.reset();
this.listDiv.innerHTML = "";
this.pagination.classList.add("hidden");
const parameters: Record<string, any> = {
offset: (page - 1) * itemsPerPage,
limit: itemsPerPage,
};
if (tags) {
parameters.tags = tags.join(",");
}
try {
const response = await sendApiGet("blog", parameters);
if (response.ok) {
const body = await response.json();
if (body.articles.length === 0) {
throw "No articles found.";
}
for (const article of body.articles) {
const anchor = this.listItemTemplate.content.children[0].cloneNode(true) as HTMLAnchorElement;
anchor.href = `/blog?id=${article.id}`;
const heading = anchor.querySelector<HTMLHeadingElement>("h3")!;
heading.textContent = article.title;
const badgeText = getBadgeText(body.status);
if (badgeText) {
const badge = document.createElement("tesoft-badge") as TesoftBadge;
badge.textContent = badgeText;
heading.appendChild(badge);
}
anchor.querySelector<HTMLDivElement>(".dates")!.textContent = formatArticleDate(new Date(article.date), article['mod-date'] ? new Date(article['mod-date']) : null);
const tags = anchor.querySelector<HTMLDivElement>(".tags")!;
for (const tagName of article.tags) {
const badge = document.createElement("tesoft-badge") as TesoftBadge;
badge.textContent = tagName;
tags.appendChild(badge);
}
this.listDiv.appendChild(anchor);
}
this.pagination.curPage = page;
this.pagination.lastPage = Math.ceil(body.total / itemsPerPage);
this.pagination.classList.remove("hidden");
} else {
const body = await response.json();
throw `${capitalize(body.message)}.`;
}
} catch (error: any) {
this.error.textContent = error;
this.error.classList.remove("hidden");
this.listDiv.classList.add("hidden");
}
this.loaderSection.finish();
}
private async fetchTags(): Promise<string[]> {
const tags: string[] = [];
let offset = 0;
const limit = 50;
while (true) {
const response = await sendApiGet("blog/tags", {
offset: offset,
limit: limit,
});
if (!response.ok) {
const body = await response.json();
throw `${capitalize(body.message)}.`;
}
const body = await response.json();
for (const tag of body.tags) {
tags.push(tag);
}
if (body.tags.length < limit) {
break;
}
}
return tags;
}
}
customElements.define("tesoft-blog-article-list", TesoftBlogArticleList);