Ability to bulk import or export articles as gzip, handle domain prefix logic in API and other smaller improvements

This commit is contained in:
thiloho
2024-10-30 21:33:44 +01:00
parent f7180ebd92
commit 037165947b
32 changed files with 409 additions and 223 deletions

View File

@@ -1,16 +1,30 @@
<script lang="ts">
const { date }: { date: Date } = $props();
const { date }: { date: string } = $props();
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
const dateObject = new Date(date);
const calcTimeAgo = (date: Date) => {
const formatter = new Intl.RelativeTimeFormat("en");
const ranges = [
["years", 60 * 60 * 24 * 365],
["months", 60 * 60 * 24 * 30],
["weeks", 60 * 60 * 24 * 7],
["days", 60 * 60 * 24],
["hours", 60 * 60],
["minutes", 60],
["seconds", 1]
] as const;
const secondsElapsed = (date.getTime() - Date.now()) / 1000;
for (const [rangeType, rangeVal] of ranges) {
if (rangeVal < Math.abs(secondsElapsed)) {
const delta = secondsElapsed / rangeVal;
return formatter.format(Math.round(delta), rangeType);
}
}
};
</script>
<time datetime={new Date(date).toLocaleString("sv").replace(" ", "T")}>
{new Date(date).toLocaleString("en-us", { ...options })}
<time datetime={dateObject.toLocaleString("sv").replace(" ", "T")}>
{calcTimeAgo(dateObject)}
</time>

View File

@@ -9,6 +9,14 @@
}: { children: Snippet; id: string; text: string; isWider?: boolean } = $props();
const modalId = `${id}-modal`;
$effect(() => {
window.addEventListener("keydown", (e) => {
if (e.key === "Escape" && window.location.hash === `#${modalId}`) {
window.location.hash = "!";
}
});
});
</script>
<a href={`#${modalId}`} role="button">{text}</a>

View File

@@ -17,15 +17,16 @@ export interface Article {
website_id: string;
user_id: string | null;
title: string;
slug: string | null;
meta_description: string | null;
meta_author: string | null;
cover_image: string | null;
publication_date: Date | null;
publication_date: string | null;
main_content: string | null;
category: string | null;
article_weight: number | null;
created_at: Date;
last_modified_at: Date;
created_at: string;
last_modified_at: string;
last_modified_by: string | null;
}
export interface ArticleInput {
@@ -33,15 +34,16 @@ export interface ArticleInput {
website_id: string;
user_id?: string | null;
title: string;
slug?: string | null;
meta_description?: string | null;
meta_author?: string | null;
cover_image?: string | null;
publication_date?: Date | null;
publication_date?: string | null;
main_content?: string | null;
category?: string | null;
article_weight?: number | null;
created_at?: Date;
last_modified_at?: Date;
created_at?: string;
last_modified_at?: string;
last_modified_by?: string | null;
}
const article = {
@@ -51,6 +53,7 @@ const article = {
"website_id",
"user_id",
"title",
"slug",
"meta_description",
"meta_author",
"cover_image",
@@ -81,7 +84,7 @@ export interface ChangeLog {
website_id: string | null;
user_id: string | null;
username: string;
tstamp: Date;
tstamp: string;
table_name: string;
operation: string;
old_value: any | null;
@@ -92,7 +95,7 @@ export interface ChangeLogInput {
website_id?: string | null;
user_id?: string | null;
username?: string;
tstamp?: Date;
tstamp?: string;
table_name: string;
operation: string;
old_value?: any | null;
@@ -126,16 +129,16 @@ export interface Collab {
website_id: string;
user_id: string;
permission_level: number;
added_at: Date;
last_modified_at: Date;
added_at: string;
last_modified_at: string;
last_modified_by: string | null;
}
export interface CollabInput {
website_id: string;
user_id: string;
permission_level?: number;
added_at?: Date;
last_modified_at?: Date;
added_at?: string;
last_modified_at?: string;
last_modified_by?: string | null;
}
const collab = {
@@ -166,8 +169,8 @@ export interface DocsCategory {
user_id: string | null;
category_name: string;
category_weight: number;
created_at: Date;
last_modified_at: Date;
created_at: string;
last_modified_at: string;
last_modified_by: string | null;
}
export interface DocsCategoryInput {
@@ -176,8 +179,8 @@ export interface DocsCategoryInput {
user_id?: string | null;
category_name: string;
category_weight: number;
created_at?: Date;
last_modified_at?: Date;
created_at?: string;
last_modified_at?: string;
last_modified_by?: string | null;
}
const docs_category = {
@@ -207,15 +210,15 @@ const docs_category = {
export interface DomainPrefix {
website_id: string;
prefix: string;
created_at: Date;
last_modified_at: Date;
created_at: string;
last_modified_at: string;
last_modified_by: string | null;
}
export interface DomainPrefixInput {
website_id: string;
prefix: string;
created_at?: Date;
last_modified_at?: Date;
created_at?: string;
last_modified_at?: string;
last_modified_by?: string | null;
}
const domain_prefix = {
@@ -235,13 +238,13 @@ const domain_prefix = {
export interface Footer {
website_id: string;
additional_text: string;
last_modified_at: Date;
last_modified_at: string;
last_modified_by: string | null;
}
export interface FooterInput {
website_id: string;
additional_text: string;
last_modified_at?: Date;
last_modified_at?: string;
last_modified_by?: string | null;
}
const footer = {
@@ -263,7 +266,7 @@ export interface Header {
logo_type: string;
logo_text: string | null;
logo_image: string | null;
last_modified_at: Date;
last_modified_at: string;
last_modified_by: string | null;
}
export interface HeaderInput {
@@ -271,7 +274,7 @@ export interface HeaderInput {
logo_type?: string;
logo_text?: string | null;
logo_image?: string | null;
last_modified_at?: Date;
last_modified_at?: string;
last_modified_by?: string | null;
}
const header = {
@@ -300,14 +303,14 @@ export interface Home {
website_id: string;
main_content: string;
meta_description: string | null;
last_modified_at: Date;
last_modified_at: string;
last_modified_by: string | null;
}
export interface HomeInput {
website_id: string;
main_content: string;
meta_description?: string | null;
last_modified_at?: Date;
last_modified_at?: string;
last_modified_by?: string | null;
}
const home = {
@@ -333,15 +336,15 @@ const home = {
export interface LegalInformation {
website_id: string;
main_content: string;
created_at: Date;
last_modified_at: Date;
created_at: string;
last_modified_at: string;
last_modified_by: string | null;
}
export interface LegalInformationInput {
website_id: string;
main_content: string;
created_at?: Date;
last_modified_at?: Date;
created_at?: string;
last_modified_at?: string;
last_modified_by?: string | null;
}
const legal_information = {
@@ -365,7 +368,7 @@ export interface Media {
blob: string;
mimetype: string;
original_name: string;
created_at: Date;
created_at: string;
}
export interface MediaInput {
id?: string;
@@ -374,7 +377,7 @@ export interface MediaInput {
blob: string;
mimetype: string;
original_name: string;
created_at?: Date;
created_at?: string;
}
const media = {
tableName: "media",
@@ -397,7 +400,7 @@ export interface Settings {
background_color_dark_theme: string;
background_color_light_theme: string;
favicon_image: string | null;
last_modified_at: Date;
last_modified_at: string;
last_modified_by: string | null;
}
export interface SettingsInput {
@@ -407,7 +410,7 @@ export interface SettingsInput {
background_color_dark_theme?: string;
background_color_light_theme?: string;
favicon_image?: string | null;
last_modified_at?: Date;
last_modified_at?: string;
last_modified_by?: string | null;
}
const settings = {
@@ -440,7 +443,7 @@ export interface User {
password_hash: string;
user_role: string;
max_number_websites: number;
created_at: Date;
created_at: string;
}
export interface UserInput {
id?: string;
@@ -448,7 +451,7 @@ export interface UserInput {
password_hash: string;
user_role?: string;
max_number_websites?: number;
created_at?: Date;
created_at?: string;
}
const user = {
tableName: "user",
@@ -468,8 +471,8 @@ export interface Website {
title: string;
max_storage_size: number;
is_published: boolean;
created_at: Date;
last_modified_at: Date;
created_at: string;
last_modified_at: string;
last_modified_by: string | null;
}
export interface WebsiteInput {
@@ -479,8 +482,8 @@ export interface WebsiteInput {
title: string;
max_storage_size?: number;
is_published?: boolean;
created_at?: Date;
last_modified_at?: Date;
created_at?: string;
last_modified_at?: string;
last_modified_by?: string | null;
}
const website = {

View File

@@ -19,6 +19,7 @@
nestingLevel={1}
{apiUrl}
title={article.title}
slug={article.slug as string}
metaDescription={article.meta_description}
{websiteUrl}
/>

View File

@@ -2,7 +2,7 @@
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
import { md, slugify, type WebsiteOverview } from "$lib/utils";
import { md, type WebsiteOverview } from "$lib/utils";
const {
websiteOverview,
@@ -62,7 +62,7 @@
{/if}
<p>
<strong>
<a href="./articles/{slugify(article.title)}">{article.title}</a>
<a href="./articles/{article.slug}">{article.title}</a>
</strong>
</p>
{#if article.meta_description}

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import { slugify, type WebsiteOverview } from "../../utils";
import { type WebsiteOverview } from "../../utils";
const {
websiteOverview,
nestingLevel,
apiUrl,
title,
slug,
metaDescription,
websiteUrl
}: {
@@ -13,6 +14,7 @@
nestingLevel: number;
apiUrl: string;
title: string;
slug?: string;
metaDescription?: string | null;
websiteUrl: string;
} = $props();
@@ -20,7 +22,7 @@
const constructedTitle =
websiteOverview.title === title ? title : `${websiteOverview.title} | ${title}`;
let ogUrl = `${websiteUrl.replace(/\/$/, "")}${nestingLevel === 0 ? (websiteOverview.title === title ? "" : `/${slugify(title)}`) : `/articles/${slugify(title)}`}`;
let ogUrl = `${websiteUrl.replace(/\/$/, "")}${nestingLevel === 0 ? (websiteOverview.title === title ? "" : `/${slug}`) : `/articles/${slug}`}`;
</script>
<svelte:head>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { type WebsiteOverview, slugify } from "../../utils";
import { type WebsiteOverview } from "../../utils";
import type { Article } from "../../db-schema";
const {
@@ -61,9 +61,9 @@
<li>
<strong>{key}</strong>
<ul>
{#each categorizedArticles[key] as { title }}
{#each categorizedArticles[key] as { title, slug }}
<li>
<a href="{isIndexPage ? './articles' : '.'}/{slugify(title)}">{title}</a>
<a href="{isIndexPage ? './articles' : '.'}/{slug}">{title}</a>
</li>
{/each}
</ul>
@@ -87,7 +87,7 @@
/>
{/if}
</svelte:element>
<label style="margin-inline-start: auto;" for="toggle-theme">
<label for="toggle-theme">
<input type="checkbox" id="toggle-theme" hidden />
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -19,6 +19,7 @@
nestingLevel={1}
{apiUrl}
title={article.title}
slug={article.slug as string}
metaDescription={article.meta_description}
{websiteUrl}
/>

View File

@@ -26,7 +26,7 @@ export const ALLOWED_MIME_TYPES = [
"image/svg+xml"
];
export const slugify = (string: string) => {
const slugify = (string: string) => {
return string
.toString()
.normalize("NFKD") // Normalize Unicode characters