From 5a659e59cdc8b530c27fbda5969566ed94f019fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Erbsh=C3=A4u=C3=9Fer?= Date: Sun, 24 May 2026 09:22:49 +0200 Subject: [PATCH] add blog article component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tobias Erbshäußer --- frontend/src/components/blog-article.njk | 81 ++++++++++++++++++++++ frontend/src/components/blog-article.ts | 88 ++++++++++++++++++++++++ frontend/src/scripts/main.ts | 23 +++++++ frontend/src/styles/main.css | 9 ++- 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/blog-article.njk create mode 100644 frontend/src/components/blog-article.ts diff --git a/frontend/src/components/blog-article.njk b/frontend/src/components/blog-article.njk new file mode 100644 index 0000000..6082465 --- /dev/null +++ b/frontend/src/components/blog-article.njk @@ -0,0 +1,81 @@ + + + +{% includeOnce 'components/badge.njk' %} +{% includeOnce 'components/error.njk' %} +{% includeOnce 'components/loader-section.njk' %} +{% includeOnce 'components/tag.njk' %} diff --git a/frontend/src/components/blog-article.ts b/frontend/src/components/blog-article.ts new file mode 100644 index 0000000..c741e8a --- /dev/null +++ b/frontend/src/components/blog-article.ts @@ -0,0 +1,88 @@ +import {capitalize, formatDate, sendApiGet, TesoftComponent} from "../scripts/main.ts"; +import {TesoftLoaderSection} from "./loader-section.ts"; +import {TesoftBadge} from "./badge.ts"; +import {TesoftTag} from "./tag.ts"; +import {TesoftError} from "./error.ts"; + +enum ArticleStatus { + Draft = 0, + Published = 1, + Offline = 2, +} + +export class TesoftBlogArticle extends TesoftComponent { + private readonly loaderSection: TesoftLoaderSection; + private readonly article: HTMLDivElement; + private readonly heading: HTMLHeadingElement; + private readonly datesDiv: HTMLDivElement; + private readonly tagsDiv: HTMLDivElement; + private readonly contentDiv: HTMLDivElement; + private readonly error: TesoftError; + + constructor() { + super(); + + const template = document.getElementById("tesoft-blog-article-template") as HTMLTemplateElement; + const templateContent = template.content; + + const shadowRoot = this.attachShadow({mode: "open"}); + shadowRoot.appendChild(templateContent.cloneNode(true)); + + this.loaderSection = shadowRoot.querySelector("tesoft-loader-section")!; + this.article = shadowRoot.querySelector(".article")!; + this.heading = shadowRoot.querySelector("h1")!; + this.datesDiv = shadowRoot.querySelector(".dates")!; + this.tagsDiv = shadowRoot.querySelector(".tags")!; + this.contentDiv = shadowRoot.querySelector(".content")!; + this.error = shadowRoot.querySelector("tesoft-error")!; + } + + async load(id: number, constructUrl: (tag: string) => string) { + const response = await sendApiGet(`blog/${id}`, {}); + if (response.ok) { + const body = await response.json(); + const date = new Date(body.date); + const modDate = body['mod-date'] ? new Date(body['mod-date']) : null; + + this.heading.textContent = body.title; + + let badgeText: string; + switch (body.status) { + case ArticleStatus.Draft: + badgeText = "Draft"; + break; + case ArticleStatus.Offline: + badgeText = "Offline"; + break; + default: + badgeText = ""; + break; + } + if (badgeText) { + const badge = document.createElement("tesoft-badge") as TesoftBadge; + badge.textContent = badgeText; + this.heading.appendChild(badge); + } + + this.datesDiv.textContent = `Posted on ${formatDate(date)}${modDate === null ? "" : `, last modified on ${formatDate(modDate)}`}`; + + for (const tagName of body.tags) { + const tag = document.createElement("tesoft-tag") as TesoftTag; + tag.innerText = tagName; + tag.href = constructUrl(tagName); + this.tagsDiv.appendChild(tag); + } + + this.contentDiv.innerHTML = body.content; + } else { + const body = await response.json(); + this.error.textContent = capitalize(body.message); + this.error.classList.remove("hidden"); + this.article.classList.add("hidden"); + } + + this.loaderSection.finish(); + } +} + +customElements.define("tesoft-blog-article", TesoftBlogArticle); diff --git a/frontend/src/scripts/main.ts b/frontend/src/scripts/main.ts index 81d858a..9d8b19f 100644 --- a/frontend/src/scripts/main.ts +++ b/frontend/src/scripts/main.ts @@ -125,3 +125,26 @@ export function sendApiPost(path: string, data: Record): Promise): Promise { + const url = new URL(`/api/${path}`, location.origin); + for (const [key, value] of Object.entries(query)) { + url.searchParams.append(key, value.toString()); + } + + return fetch(url, { + method: "GET", + }); +} + +export function formatDate(date: Date): string { + return `${date.getFullYear().toString().padStart(4, "0")}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`; +} diff --git a/frontend/src/styles/main.css b/frontend/src/styles/main.css index 870b716..3606cad 100644 --- a/frontend/src/styles/main.css +++ b/frontend/src/styles/main.css @@ -10,6 +10,11 @@ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; } +html, body { + min-height: 100vh; + overflow: auto; +} + body { background-color: var(--dark-gray); color: var(--light-gray); @@ -24,12 +29,14 @@ body > div { } tesoft-nav { + padding-bottom: 0.8rem; position: sticky; top: 0; } main { - padding: 0 0.5rem 0 0.5rem; + min-height: calc(100vh - 8rem); + padding: 0 0.5rem; } @media (max-width: 1440px) {