add blog article component
Signed-off-by: Tobias Erbshäußer <tobias@tesoft.dev>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
<template id="tesoft-blog-article-template">
|
||||
<style>
|
||||
@import "/src/styles/default.css";
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
tesoft-loader-section {
|
||||
min-height: calc(100vh - 8rem);
|
||||
}
|
||||
|
||||
.article {
|
||||
display: grid;
|
||||
gap: 1.6rem;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
}
|
||||
|
||||
.header > h1 {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: auto auto;
|
||||
grid-template-rows: auto;
|
||||
justify-content: start;
|
||||
font-size: 4.4rem;
|
||||
line-height: 4.4rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.dates {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
tesoft-error {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<tesoft-loader-section>
|
||||
<div class="article">
|
||||
<div class="header">
|
||||
<h1></h1>
|
||||
<div class="dates">
|
||||
</div>
|
||||
<div class="tags">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content"></div>
|
||||
</div>
|
||||
|
||||
<tesoft-error class="hidden">
|
||||
</tesoft-error>
|
||||
</tesoft-loader-section>
|
||||
</template>
|
||||
|
||||
<script src="/src/components/blog-article.ts" type="module"></script>
|
||||
{% includeOnce 'components/badge.njk' %}
|
||||
{% includeOnce 'components/error.njk' %}
|
||||
{% includeOnce 'components/loader-section.njk' %}
|
||||
{% includeOnce 'components/tag.njk' %}
|
||||
@@ -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<TesoftLoaderSection>("tesoft-loader-section")!;
|
||||
this.article = shadowRoot.querySelector<HTMLDivElement>(".article")!;
|
||||
this.heading = shadowRoot.querySelector<HTMLHeadingElement>("h1")!;
|
||||
this.datesDiv = shadowRoot.querySelector<HTMLDivElement>(".dates")!;
|
||||
this.tagsDiv = shadowRoot.querySelector<HTMLDivElement>(".tags")!;
|
||||
this.contentDiv = shadowRoot.querySelector<HTMLDivElement>(".content")!;
|
||||
this.error = shadowRoot.querySelector<TesoftError>("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);
|
||||
@@ -125,3 +125,26 @@ export function sendApiPost(path: string, data: Record<string, any>): Promise<Re
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export function capitalize(str: string): string {
|
||||
if (str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export function sendApiGet(path: string, query: Record<string, string | number>): Promise<Response> {
|
||||
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")}`;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user