Add TypeScript definitions via pg-to-ts and refactor migrations

This commit is contained in:
thiloho
2024-09-10 17:29:57 +02:00
parent 8121be1d96
commit c5fbcdc8bd
50 changed files with 1525 additions and 1632 deletions

View File

@@ -1,9 +1,6 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
interface User {
id: string;
username: string;
}
import type { User } from "$lib/db-schema";
declare global {
namespace App {

View File

@@ -1,5 +1,5 @@
<script lang="ts">
const { date }: { date: string } = $props();
const { date }: { date: Date } = $props();
const options: Intl.DateTimeFormatOptions = {
year: "numeric",
@@ -11,6 +11,6 @@
};
</script>
<time datetime={new Date(date).toLocaleString("sv").replace(" ", "T")}>
{new Date(date).toLocaleString("en-us", { ...options })}
<time datetime={date.toLocaleString("sv").replace(" ", "T")}>
{date.toLocaleString("en-us", { ...options })}
</time>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
const { success, message }: { success: boolean | undefined; message: string | undefined } =
$props();
const { success, message }: { success?: boolean; message?: string } = $props();
</script>
{#if success}

View File

@@ -0,0 +1,496 @@
/* tslint:disable */
/* eslint-disable */
/**
* AUTO-GENERATED FILE - DO NOT EDIT!
*
* This file was automatically generated by pg-to-ts v.4.1.1
* $ pg-to-ts generate -c postgres://username:password@localhost:15432/archtika -t article -t change_log -t collab -t docs_category -t footer -t header -t home -t legal_information -t media -t settings -t user -t website -s internal
*
*/
export type Json = unknown;
// Table article
export interface Article {
id: string;
website_id: string;
user_id: string | null;
title: string;
meta_description: string | null;
meta_author: string | null;
cover_image: string | null;
publication_date: Date | null;
main_content: string | null;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
title_description_search: any | null;
category: string | null;
article_weight: number | null;
}
export interface ArticleInput {
id?: string;
website_id: string;
user_id?: string | null;
title: string;
meta_description?: string | null;
meta_author?: string | null;
cover_image?: string | null;
publication_date?: Date | null;
main_content?: string | null;
created_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
title_description_search?: any | null;
category?: string | null;
article_weight?: number | null;
}
const article = {
tableName: "article",
columns: [
"id",
"website_id",
"user_id",
"title",
"meta_description",
"meta_author",
"cover_image",
"publication_date",
"main_content",
"created_at",
"last_modified_at",
"last_modified_by",
"title_description_search",
"category",
"article_weight"
],
requiredForInsert: ["website_id", "title"],
primaryKey: "id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User },
cover_image: { table: "media", column: "id", $type: null as unknown as Media },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User },
category: { table: "docs_category", column: "id", $type: null as unknown as DocsCategory }
},
$type: null as unknown as Article,
$input: null as unknown as ArticleInput
} as const;
// Table change_log
export interface ChangeLog {
website_id: string;
user_id: string;
change_summary: string;
previous_value: Json | null;
new_value: Json | null;
timestamp: Date;
}
export interface ChangeLogInput {
website_id: string;
user_id?: string;
change_summary: string;
previous_value?: Json | null;
new_value?: Json | null;
timestamp?: Date;
}
const change_log = {
tableName: "change_log",
columns: ["website_id", "user_id", "change_summary", "previous_value", "new_value", "timestamp"],
requiredForInsert: ["website_id", "change_summary"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as ChangeLog,
$input: null as unknown as ChangeLogInput
} as const;
// Table collab
export interface Collab {
website_id: string;
user_id: string;
permission_level: number;
added_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface CollabInput {
website_id: string;
user_id: string;
permission_level?: number;
added_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const collab = {
tableName: "collab",
columns: [
"website_id",
"user_id",
"permission_level",
"added_at",
"last_modified_at",
"last_modified_by"
],
requiredForInsert: ["website_id", "user_id"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Collab,
$input: null as unknown as CollabInput
} as const;
// Table docs_category
export interface DocsCategory {
id: string;
website_id: string;
user_id: string | null;
category_name: string;
category_weight: number;
}
export interface DocsCategoryInput {
id?: string;
website_id: string;
user_id?: string | null;
category_name: string;
category_weight: number;
}
const docs_category = {
tableName: "docs_category",
columns: ["id", "website_id", "user_id", "category_name", "category_weight"],
requiredForInsert: ["website_id", "category_name", "category_weight"],
primaryKey: "id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as DocsCategory,
$input: null as unknown as DocsCategoryInput
} as const;
// Table footer
export interface Footer {
website_id: string;
additional_text: string;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface FooterInput {
website_id: string;
additional_text: string;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const footer = {
tableName: "footer",
columns: ["website_id", "additional_text", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "additional_text"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Footer,
$input: null as unknown as FooterInput
} as const;
// Table header
export interface Header {
website_id: string;
logo_type: string;
logo_text: string | null;
logo_image: string | null;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface HeaderInput {
website_id: string;
logo_type?: string;
logo_text?: string | null;
logo_image?: string | null;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const header = {
tableName: "header",
columns: [
"website_id",
"logo_type",
"logo_text",
"logo_image",
"last_modified_at",
"last_modified_by"
],
requiredForInsert: ["website_id"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
logo_image: { table: "media", column: "id", $type: null as unknown as Media },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Header,
$input: null as unknown as HeaderInput
} as const;
// Table home
export interface Home {
website_id: string;
main_content: string;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface HomeInput {
website_id: string;
main_content: string;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const home = {
tableName: "home",
columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "main_content"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Home,
$input: null as unknown as HomeInput
} as const;
// Table legal_information
export interface LegalInformation {
website_id: string;
main_content: string;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface LegalInformationInput {
website_id: string;
main_content: string;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const legal_information = {
tableName: "legal_information",
columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "main_content"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as LegalInformation,
$input: null as unknown as LegalInformationInput
} as const;
// Table media
export interface Media {
id: string;
website_id: string;
user_id: string;
blob: string;
mimetype: string;
original_name: string;
created_at: Date;
}
export interface MediaInput {
id?: string;
website_id: string;
user_id?: string;
blob: string;
mimetype: string;
original_name: string;
created_at?: Date;
}
const media = {
tableName: "media",
columns: ["id", "website_id", "user_id", "blob", "mimetype", "original_name", "created_at"],
requiredForInsert: ["website_id", "blob", "mimetype", "original_name"],
primaryKey: "id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
user_id: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Media,
$input: null as unknown as MediaInput
} as const;
// Table settings
export interface Settings {
website_id: string;
accent_color_light_theme: string;
accent_color_dark_theme: string;
favicon_image: string | null;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface SettingsInput {
website_id: string;
accent_color_light_theme?: string;
accent_color_dark_theme?: string;
favicon_image?: string | null;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const settings = {
tableName: "settings",
columns: [
"website_id",
"accent_color_light_theme",
"accent_color_dark_theme",
"favicon_image",
"last_modified_at",
"last_modified_by"
],
requiredForInsert: ["website_id"],
primaryKey: "website_id",
foreignKeys: {
website_id: { table: "website", column: "id", $type: null as unknown as Website },
favicon_image: { table: "media", column: "id", $type: null as unknown as Media },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Settings,
$input: null as unknown as SettingsInput
} as const;
// Table user
export interface User {
id: string;
username: string;
password_hash: string;
role: string;
}
export interface UserInput {
id?: string;
username: string;
password_hash: string;
role?: string;
}
const user = {
tableName: "user",
columns: ["id", "username", "password_hash", "role"],
requiredForInsert: ["username", "password_hash"],
primaryKey: "id",
foreignKeys: {},
$type: null as unknown as User,
$input: null as unknown as UserInput
} as const;
// Table website
export interface Website {
id: string;
user_id: string;
content_type: string;
title: string;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
title_search: any | null;
is_published: boolean;
}
export interface WebsiteInput {
id?: string;
user_id?: string;
content_type: string;
title: string;
created_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
title_search?: any | null;
is_published?: boolean;
}
const website = {
tableName: "website",
columns: [
"id",
"user_id",
"content_type",
"title",
"created_at",
"last_modified_at",
"last_modified_by",
"title_search",
"is_published"
],
requiredForInsert: ["content_type", "title"],
primaryKey: "id",
foreignKeys: {
user_id: { table: "user", column: "id", $type: null as unknown as User },
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
},
$type: null as unknown as Website,
$input: null as unknown as WebsiteInput
} as const;
export interface TableTypes {
article: {
select: Article;
input: ArticleInput;
};
change_log: {
select: ChangeLog;
input: ChangeLogInput;
};
collab: {
select: Collab;
input: CollabInput;
};
docs_category: {
select: DocsCategory;
input: DocsCategoryInput;
};
footer: {
select: Footer;
input: FooterInput;
};
header: {
select: Header;
input: HeaderInput;
};
home: {
select: Home;
input: HomeInput;
};
legal_information: {
select: LegalInformation;
input: LegalInformationInput;
};
media: {
select: Media;
input: MediaInput;
};
settings: {
select: Settings;
input: SettingsInput;
};
user: {
select: User;
input: UserInput;
};
website: {
select: Website;
input: WebsiteInput;
};
}
export const tables = {
article,
change_log,
collab,
docs_category,
footer,
header,
home,
legal_information,
media,
settings,
user,
website
};

View File

@@ -2,52 +2,44 @@
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
import { type WebsiteOverview, md } from "../../utils";
import type { Article } from "../../db-schema";
const {
favicon,
title,
logoType,
logo,
mainContent,
coverImage,
publicationDate,
footerAdditionalText,
metaDescription
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
coverImage: string;
publicationDate: string;
footerAdditionalText: string;
metaDescription: string;
} = $props();
websiteOverview,
article,
apiUrl
}: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string } = $props();
</script>
<Head {title} {favicon} nestingLevel={1} {metaDescription} />
<Head
{websiteOverview}
nestingLevel={1}
{apiUrl}
title={article.title}
metaDescription={article.meta_description}
/>
<Nav {logoType} {logo} isIndexPage={false} />
<Nav {websiteOverview} isDocsTemplate={false} isIndexPage={false} {apiUrl} />
<header>
<div class="container">
<hgroup>
<p>{publicationDate}</p>
<h1>{title}</h1>
<p>{article.publication_date}</p>
<h1>{article.title}</h1>
</hgroup>
{#if coverImage}
<img src={coverImage} alt="" />
{#if article.cover_image}
<img src="{apiUrl}/rpc/retrieve_file?id={article.cover_image}" alt="" />
{/if}
</div>
</header>
{#if mainContent}
{#if article.main_content}
<main>
<div class="container">
{@html mainContent}
{@html md(article.main_content)}
</div>
</main>
{/if}
<Footer text={footerAdditionalText} isIndexPage={false} />
<Footer {websiteOverview} isIndexPage={false} />

View File

@@ -2,47 +2,46 @@
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
import { md, type WebsiteOverview } from "../../utils";
const {
favicon,
title,
logoType,
logo,
mainContent,
articles,
footerAdditionalText
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
articles: { title: string; publication_date: string; meta_description: string }[];
footerAdditionalText: string;
} = $props();
websiteOverview,
apiUrl,
isLegalPage
}: { websiteOverview: WebsiteOverview; apiUrl: string; isLegalPage: boolean } = $props();
</script>
<Head {title} {favicon} />
<Head
{websiteOverview}
nestingLevel={0}
{apiUrl}
title={isLegalPage ? "Legal information" : websiteOverview.title}
/>
<Nav {logoType} {logo} />
<Nav {websiteOverview} isDocsTemplate={false} isIndexPage={true} {apiUrl} />
<header>
<div class="container">
<h1>{title}</h1>
<h1>{isLegalPage ? "Legal information" : websiteOverview.title}</h1>
</div>
</header>
<main>
<div class="container">
{@html mainContent}
{#if articles.length > 0}
{@html md(
isLegalPage
? (websiteOverview.legal_information?.main_content ?? "")
: websiteOverview.home.main_content,
false
)}
{#if websiteOverview.article.length > 0 && !isLegalPage}
<section class="articles" id="articles">
<h2>
<a href="#articles">Articles</a>
</h2>
<ul class="unpadded">
{#each articles as article}
{#each websiteOverview.article as article}
{@const articleFileName = article.title.toLowerCase().split(" ").join("-")}
<li>
<p>{article.publication_date}</p>
@@ -62,4 +61,4 @@
</div>
</main>
<Footer text={footerAdditionalText} />
<Footer {websiteOverview} isIndexPage={true} />

View File

@@ -1,11 +1,16 @@
<script lang="ts">
const { text, isIndexPage = true }: { text: string; isIndexPage?: boolean } = $props();
import type { WebsiteOverview } from "../../utils";
const {
websiteOverview,
isIndexPage
}: { websiteOverview: WebsiteOverview; isIndexPage: boolean } = $props();
</script>
<footer>
<div class="container">
<small>
{@html text.replace(
{@html websiteOverview.footer.additional_text.replace(
"!!legal",
`<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>`
)}

View File

@@ -1,13 +1,17 @@
<script lang="ts">
import type { WebsiteOverview } from "../../utils";
const {
websiteOverview,
nestingLevel,
apiUrl,
title,
favicon,
nestingLevel = 0,
metaDescription = null
metaDescription
}: {
websiteOverview: WebsiteOverview;
nestingLevel: number;
apiUrl: string;
title: string;
favicon: string;
nestingLevel?: number;
metaDescription?: string | null;
} = $props();
</script>
@@ -19,8 +23,11 @@
<title>{title}</title>
<meta name="description" content={metaDescription ?? title} />
<link rel="stylesheet" href={`${"../".repeat(nestingLevel)}styles.css`} />
{#if favicon}
<link rel="icon" href={favicon} />
{#if websiteOverview.settings.favicon_image}
<link
rel="icon"
href="{apiUrl}/rpc/retrieve_file?id={websiteOverview.settings.favicon_image}"
/>
{/if}
</head>
</svelte:head>

View File

@@ -1,17 +1,36 @@
<script lang="ts">
import type { WebsiteOverview } from "../../utils";
import type { Article } from "../../db-schema";
const {
logoType,
logo,
isDocsTemplate = false,
categorizedArticles = {},
isIndexPage = true
websiteOverview,
isDocsTemplate,
isIndexPage,
apiUrl
}: {
logoType: "text" | "image";
logo: string;
isDocsTemplate?: boolean;
categorizedArticles?: { [key: string]: { title: string }[] };
isIndexPage?: boolean;
websiteOverview: WebsiteOverview;
isDocsTemplate: boolean;
isIndexPage: boolean;
apiUrl: string;
} = $props();
const categorizedArticles = Object.fromEntries(
Object.entries(
Object.groupBy(
websiteOverview.article.sort((a, b) => (b.article_weight ?? 0) - (a.article_weight ?? 0)),
(article) => article.docs_category?.category_name ?? "Uncategorized"
)
).sort(([a], [b]) =>
a === "Uncategorized"
? 1
: b === "Uncategorized"
? -1
: (websiteOverview.article.find((art) => art.docs_category?.category_name === b)
?.docs_category?.category_weight ?? 0) -
(websiteOverview.article.find((art) => art.docs_category?.category_name === a)
?.docs_category?.category_weight ?? 0)
)
) as { [key: string]: Article[] };
</script>
<nav>
@@ -53,10 +72,15 @@
</section>
{/if}
<a href={isIndexPage ? "." : ".."}>
{#if logoType === "text"}
<strong>{logo}</strong>
{#if websiteOverview.header.logo_type === "text"}
<strong>{websiteOverview.header.logo_text}</strong>
{:else}
<img src={logo} width="24" height="24" alt="" />
<img
src="{apiUrl}/rpc/retrieve_file?id={websiteOverview.header.logo_image}"
width="24"
height="24"
alt=""
/>
{/if}
</a>
</div>

View File

@@ -2,44 +2,38 @@
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
import { md, type WebsiteOverview } from "../../utils";
import type { Article } from "../../db-schema";
const {
favicon,
title,
logoType,
logo,
mainContent,
categorizedArticles,
footerAdditionalText,
metaDescription
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
categorizedArticles: { [key: string]: { title: string }[] };
footerAdditionalText: string;
metaDescription: string;
} = $props();
websiteOverview,
article,
apiUrl
}: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string } = $props();
</script>
<Head {title} {favicon} nestingLevel={1} {metaDescription} />
<Head
{websiteOverview}
nestingLevel={1}
{apiUrl}
title={article.title}
metaDescription={article.meta_description}
/>
<Nav {logoType} {logo} isDocsTemplate={true} {categorizedArticles} isIndexPage={false} />
<Nav {websiteOverview} isDocsTemplate={true} isIndexPage={false} {apiUrl} />
<header>
<div class="container">
<h1>{title}</h1>
<h1>{article.title}</h1>
</div>
</header>
{#if mainContent}
{#if article.main_content}
<main>
<div class="container">
{@html mainContent}
{@html md(article.main_content)}
</div>
</main>
{/if}
<Footer text={footerAdditionalText} isIndexPage={false} />
<Footer {websiteOverview} isIndexPage={false} />

View File

@@ -2,40 +2,39 @@
import Head from "../common/Head.svelte";
import Nav from "../common/Nav.svelte";
import Footer from "../common/Footer.svelte";
import { md, type WebsiteOverview } from "../../utils";
const {
favicon,
title,
logoType,
logo,
mainContent,
categorizedArticles,
footerAdditionalText
}: {
favicon: string;
title: string;
logoType: "text" | "image";
logo: string;
mainContent: string;
categorizedArticles: { [key: string]: { title: string }[] };
footerAdditionalText: string;
} = $props();
websiteOverview,
apiUrl,
isLegalPage
}: { websiteOverview: WebsiteOverview; apiUrl: string; isLegalPage: boolean } = $props();
</script>
<Head {title} {favicon} />
<Head
{websiteOverview}
nestingLevel={0}
{apiUrl}
title={isLegalPage ? "Legal information" : websiteOverview.title}
/>
<Nav {logoType} {logo} isDocsTemplate={true} {categorizedArticles} />
<Nav {websiteOverview} isDocsTemplate={true} isIndexPage={true} {apiUrl} />
<header>
<div class="container">
<h1>{title}</h1>
<h1>{isLegalPage ? "Legal information" : websiteOverview.title}</h1>
</div>
</header>
<main>
<div class="container">
{@html mainContent}
{@html md(
isLegalPage
? (websiteOverview.legal_information?.main_content ?? "")
: websiteOverview.home.main_content,
false
)}
</div>
</main>
<Footer text={footerAdditionalText} />
<Footer {websiteOverview} isIndexPage={true} />

View File

@@ -5,6 +5,16 @@ import hljs from "highlight.js";
import GithubSlugger from "github-slugger";
import DOMPurify from "isomorphic-dompurify";
import { applyAction, deserialize } from "$app/forms";
import type {
Website,
Settings,
Header,
Home,
Footer,
Article,
DocsCategory,
LegalInformation
} from "$lib/db-schema";
export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"];
@@ -189,3 +199,12 @@ export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: s
return "";
}
};
export interface WebsiteOverview extends Website {
settings: Settings;
header: Header;
home: Home;
footer: Footer;
article: (Article & { docs_category: DocsCategory | null })[];
legal_information?: LegalInformation;
}

View File

@@ -10,7 +10,7 @@ export const actions: Actions = {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: data.get("username"),
password: data.get("password")
pass: data.get("password")
})
});

View File

@@ -10,7 +10,7 @@ export const actions: Actions = {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: data.get("username"),
password: data.get("password")
pass: data.get("password")
})
});

View File

@@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import type { Website, WebsiteInput } from "$lib/db-schema";
export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
const searchQuery = url.searchParams.get("website_search_query");
@@ -47,7 +48,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
}
});
const websites = await websiteData.json();
const websites: Website[] = await websiteData.json();
return {
totalWebsiteCount,
@@ -66,9 +67,9 @@ export const actions: Actions = {
Authorization: `Bearer ${cookies.get("session_token")}`
},
body: JSON.stringify({
content_type: data.get("content-type"),
title: data.get("title")
})
content_type: data.get("content-type") as string,
title: data.get("title") as string
} satisfies WebsiteInput)
});
if (!res.ok) {

View File

@@ -23,7 +23,7 @@ export const actions: Actions = {
Authorization: `Bearer ${cookies.get("session_token")}`
},
body: JSON.stringify({
password: data.get("password")
pass: data.get("password")
})
});

View File

@@ -1,6 +1,7 @@
import type { LayoutServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import { error } from "@sveltejs/kit";
import type { Website, Home } from "$lib/db-schema";
export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => {
const websiteData = await fetch(`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}`, {
@@ -25,8 +26,8 @@ export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => {
}
});
const website = await websiteData.json();
const home = await homeData.json();
const website: Website = await websiteData.json();
const home: Home = await homeData.json();
return {
website,

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Settings, Header, Footer } from "$lib/db-schema";
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
const globalSettingsData = await fetch(
@@ -32,9 +33,9 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
}
});
const globalSettings = await globalSettingsData.json();
const header = await headerData.json();
const footer = await footerData.json();
const globalSettings: Settings = await globalSettingsData.json();
const header: Header = await headerData.json();
const footer: Footer = await footerData.json();
return {
globalSettings,

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Article, ArticleInput, DocsCategory } from "$lib/db-schema";
export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent, locals }) => {
const searchQuery = url.searchParams.get("article_search_query");
@@ -54,7 +55,7 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent
}
});
const articles = await articlesData.json();
const articles: (Article & { docs_category: DocsCategory | null })[] = await articlesData.json();
return {
totalArticleCount,
@@ -76,8 +77,8 @@ export const actions: Actions = {
},
body: JSON.stringify({
website_id: params.websiteId,
title: data.get("title")
})
title: data.get("title") as string
} satisfies ArticleInput)
});
if (!res.ok) {

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Article, DocsCategory } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
const articleData = await fetch(`${API_BASE_PREFIX}/article?id=eq.${params.articleId}`, {
@@ -22,8 +23,8 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
}
);
const article = await articleData.json();
const categories = await categoryData.json();
const article: Article = await articleData.json();
const categories: DocsCategory[] = await categoryData.json();
const { website } = await parent();
return { website, article, categories, API_BASE_PREFIX };

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import type { DocsCategory, DocsCategoryInput } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
const categoryData = await fetch(
@@ -13,7 +14,7 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
}
);
const categories = await categoryData.json();
const categories: DocsCategory[] = await categoryData.json();
const { website, home } = await parent();
return {
@@ -35,9 +36,9 @@ export const actions: Actions = {
},
body: JSON.stringify({
website_id: params.websiteId,
category_name: data.get("category-name"),
category_weight: data.get("category-weight")
})
category_name: data.get("category-name") as string,
category_weight: data.get("category-weight") as unknown as number
} satisfies DocsCategoryInput)
});
if (!res.ok) {

View File

@@ -1,5 +1,6 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Collab, CollabInput, User } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) => {
const { website, home } = await parent();
@@ -15,7 +16,7 @@ export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) =
}
);
const collaborators = await collabData.json();
const collaborators: (Collab & { user: User })[] = await collabData.json();
return {
website,
@@ -37,6 +38,8 @@ export const actions: Actions = {
}
});
const user: User = await userData.json();
const res = await fetch(`${API_BASE_PREFIX}/collab`, {
method: "POST",
headers: {
@@ -45,9 +48,9 @@ export const actions: Actions = {
},
body: JSON.stringify({
website_id: params.websiteId,
user_id: (await userData.json()).id,
permission_level: data.get("permission-level")
})
user_id: user.id,
permission_level: data.get("permission-level") as unknown as number
} satisfies CollabInput)
});
if (!res.ok) {

View File

@@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import type { LegalInformation, LegalInformationInput } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) => {
const legalInformationData = await fetch(
@@ -16,7 +17,7 @@ export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) =
}
);
const legalInformation = legalInformationData.ok ? await legalInformationData.json() : null;
const legalInformation: LegalInformation = await legalInformationData.json();
const { website } = await parent();
return {
@@ -39,8 +40,8 @@ export const actions: Actions = {
},
body: JSON.stringify({
website_id: params.websiteId,
main_content: data.get("main-content")
})
main_content: data.get("main-content") as string
} satisfies LegalInformationInput)
});
if (!res.ok) {

View File

@@ -8,7 +8,7 @@
const { data, form }: { data: PageServerData; form: ActionData } = $props();
let previewContent = $state(data.legalInformation?.main_content);
let previewContent = $state(data.legalInformation.main_content);
let mainContentTextarea: HTMLTextAreaElement;
let textareaScrollTop = $state(0);
@@ -80,14 +80,14 @@
bind:value={previewContent}
bind:this={mainContentTextarea}
onscroll={updateScrollPercentage}
required>{data.legalInformation?.main_content ?? ""}</textarea
required>{data.legalInformation.main_content ?? ""}</textarea
>
</label>
<button type="submit">Submit</button>
</form>
{#if data.legalInformation?.main_content}
{#if data.legalInformation.main_content}
<Modal id="delete-legal-information" text="Delete">
<form
action="?/deleteLegalInformation"
@@ -98,7 +98,6 @@
await update();
window.location.hash = "!";
sending = false;
previewContent = null;
};
}}
>

View File

@@ -1,6 +1,6 @@
import { readFile, mkdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { md } from "$lib/utils";
import { type WebsiteOverview } from "$lib/utils";
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import { render } from "svelte/server";
@@ -10,34 +10,9 @@ import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
import { dev } from "$app/environment";
interface WebsiteData {
id: string;
content_type: "Blog" | "Docs";
favicon_image: string | null;
title: string;
logo_type: "text" | "image";
logo_text: string | null;
logo_image: string | null;
main_content: string;
additional_text: string;
accent_color_light_theme: string;
accent_color_dark_theme: string;
articles: {
cover_image: string | null;
title: string;
publication_date: string;
meta_description: string;
main_content: string;
}[];
categorized_articles: {
[key: string]: { title: string; publication_date: string; meta_description: string }[];
};
legal_information_main_content: string | null;
}
export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) => {
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
const websiteOverviewData = await fetch(
`${API_BASE_PREFIX}/website_overview?id=eq.${params.websiteId}`,
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
{
method: "GET",
headers: {
@@ -48,8 +23,7 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) =
}
);
const websiteOverview = await websiteOverviewData.json();
const { website } = await parent();
const websiteOverview: WebsiteOverview = await websiteOverviewData.json();
generateStaticFiles(websiteOverview);
@@ -70,15 +44,14 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) =
return {
websiteOverview,
websitePreviewUrl,
websiteProdUrl,
website
websiteProdUrl
};
};
export const actions: Actions = {
publishWebsite: async ({ fetch, params, cookies }) => {
const websiteOverviewData = await fetch(
`${API_BASE_PREFIX}/website_overview?id=eq.${params.websiteId}`,
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
{
method: "GET",
headers: {
@@ -112,54 +85,9 @@ export const actions: Actions = {
}
};
const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean = true) => {
let head = "";
let body = "";
switch (websiteData.content_type) {
case "Blog":
{
({ head, body } = render(BlogIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: websiteData.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.main_content ?? "", false),
articles: websiteData.articles ?? [],
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
case "Docs":
{
({ head, body } = render(DocsIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: websiteData.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.main_content ?? "", false),
categorizedArticles: websiteData.categorized_articles ?? [],
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
}
const indexFileContents = `
const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: boolean = true) => {
const fileContents = (head: string, body: string) => {
return `
<!DOCTYPE html>
<html lang="en">
<head>
@@ -169,6 +97,15 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
${body}
</body>
</html>`;
};
const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
props: {
websiteOverview: websiteData,
apiUrl: API_BASE_PREFIX,
isLegalPage: false
}
});
let uploadDir = "";
@@ -179,138 +116,38 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
}
await mkdir(uploadDir, { recursive: true });
await writeFile(join(uploadDir, "index.html"), indexFileContents);
await writeFile(join(uploadDir, "index.html"), fileContents(head, body));
await mkdir(join(uploadDir, "articles"), {
recursive: true
});
for (const article of websiteData.articles ?? []) {
for (const article of websiteData.article ?? []) {
const articleFileName = article.title.toLowerCase().split(" ").join("-");
let head = "";
let body = "";
const { head, body } = render(websiteData.content_type === "Blog" ? BlogArticle : DocsArticle, {
props: {
websiteOverview: websiteData,
article,
apiUrl: API_BASE_PREFIX
}
});
switch (websiteData.content_type) {
case "Blog":
{
({ head, body } = render(BlogArticle, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: article.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
coverImage: article.cover_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}`
: "",
publicationDate: article.publication_date,
mainContent: md(article.main_content ?? ""),
footerAdditionalText: md(websiteData.additional_text ?? ""),
metaDescription: article.meta_description
}
}));
}
break;
case "Docs":
{
({ head, body } = render(DocsArticle, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: article.title,
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(article.main_content ?? ""),
categorizedArticles: websiteData.categorized_articles ?? [],
footerAdditionalText: md(websiteData.additional_text ?? ""),
metaDescription: article.meta_description
}
}));
}
break;
}
const articleFileContents = `
<!DOCTYPE html>
<html lang="en">
<head>
${head}
</head>
<body>
${body}
</body>
</html>`;
await writeFile(join(uploadDir, "articles", `${articleFileName}.html`), articleFileContents);
await writeFile(
join(uploadDir, "articles", `${articleFileName}.html`),
fileContents(head, body)
);
}
if (websiteData.legal_information_main_content) {
let head = "";
let body = "";
if (websiteData.legal_information) {
const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
props: {
websiteOverview: websiteData,
apiUrl: API_BASE_PREFIX,
isLegalPage: true
}
});
switch (websiteData.content_type) {
case "Blog":
{
({ head, body } = render(BlogIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: "Legal information",
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.legal_information_main_content ?? "", false),
articles: [],
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
case "Docs":
{
({ head, body } = render(DocsIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: "Legal information",
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.legal_information_main_content ?? "", false),
categorizedArticles: {},
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
}
const legalInformationFileContents = `
<!DOCTYPE html>
<html lang="en">
<head>
${head}
</head>
<body>
${body}
</body>
</html>`;
await writeFile(join(uploadDir, "legal-information.html"), legalInformationFileContents);
await writeFile(join(uploadDir, "legal-information.html"), fileContents(head, body));
}
const commonStyles = await readFile(`${process.cwd()}/template-styles/common-styles.css`, {
@@ -328,14 +165,14 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
.concat(specificStyles)
.replace(
/--color-accent:\s*(.*?);/,
`--color-accent: ${websiteData.accent_color_dark_theme};`
`--color-accent: ${websiteData.settings.accent_color_dark_theme};`
)
.replace(
/@media\s*\(prefers-color-scheme:\s*dark\)\s*{[^}]*--color-accent:\s*(.*?);/,
(match) =>
match.replace(
/--color-accent:\s*(.*?);/,
`--color-accent: ${websiteData.accent_color_light_theme};`
`--color-accent: ${websiteData.settings.accent_color_light_theme};`
)
)
);

View File

@@ -17,9 +17,9 @@
{/if}
<WebsiteEditor
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
id={data.websiteOverview.id}
contentType={data.websiteOverview.content_type}
title={data.websiteOverview.title}
previewContent={data.websitePreviewUrl}
fullPreview={true}
>
@@ -46,7 +46,7 @@
<button type="submit">Publish</button>
</form>
{#if data.website.is_published}
{#if data.websiteOverview.is_published}
<section id="publication-status">
<h3>
<a href="#publication-status">Publication status</a>