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),
|
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;
|
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 {
|
body {
|
||||||
background-color: var(--dark-gray);
|
background-color: var(--dark-gray);
|
||||||
color: var(--light-gray);
|
color: var(--light-gray);
|
||||||
@@ -24,12 +29,14 @@ body > div {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tesoft-nav {
|
tesoft-nav {
|
||||||
|
padding-bottom: 0.8rem;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 0 0.5rem 0 0.5rem;
|
min-height: calc(100vh - 8rem);
|
||||||
|
padding: 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1440px) {
|
@media (max-width: 1440px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user