mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 10:51:36 +01:00
Ability to bulk import or export articles as gzip, handle domain prefix logic in API and other smaller improvements
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
nestingLevel={1}
|
||||
{apiUrl}
|
||||
title={article.title}
|
||||
slug={article.slug as string}
|
||||
metaDescription={article.meta_description}
|
||||
{websiteUrl}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
nestingLevel={1}
|
||||
{apiUrl}
|
||||
title={article.title}
|
||||
slug={article.slug as string}
|
||||
metaDescription={article.meta_description}
|
||||
{websiteUrl}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user