Refactor web app code and add background color setting

This commit is contained in:
thiloho
2024-09-27 16:59:29 +02:00
parent 5fcfeffa84
commit b3b499e218
28 changed files with 375 additions and 493 deletions

View File

@@ -53,8 +53,10 @@ CREATE TABLE internal.media (
CREATE TABLE internal.settings ( CREATE TABLE internal.settings (
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE, website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
accent_color_light_theme CHAR(7) CHECK (accent_color_light_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#a5d8ff', accent_color_dark_theme CHAR(7) CHECK (accent_color_light_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#a5d8ff',
accent_color_dark_theme CHAR(7) CHECK (accent_color_dark_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#114678', accent_color_light_theme CHAR(7) CHECK (accent_color_dark_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#114678',
background_color_dark_theme CHAR(7) CHECK (accent_color_light_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#262626',
background_color_light_theme CHAR(7) CHECK (accent_color_dark_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#ffffff',
favicon_image UUID REFERENCES internal.media (id) ON DELETE SET NULL, favicon_image UUID REFERENCES internal.media (id) ON DELETE SET NULL,
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL

View File

@@ -131,7 +131,7 @@ GRANT SELECT, UPDATE (title, is_published), DELETE ON internal.website TO authen
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user; GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
GRANT SELECT, UPDATE (accent_color_light_theme, accent_color_dark_theme, favicon_image) ON internal.settings TO authenticated_user; GRANT SELECT, UPDATE (accent_color_dark_theme, accent_color_light_theme, background_color_dark_theme, background_color_light_theme, favicon_image) ON internal.settings TO authenticated_user;
GRANT SELECT, UPDATE ON api.settings TO authenticated_user; GRANT SELECT, UPDATE ON api.settings TO authenticated_user;

View File

@@ -0,0 +1,72 @@
<script lang="ts">
import { deserialize, applyAction } from "$app/forms";
import { textareaScrollTop, previewContent } from "$lib/runes.svelte";
const {
apiPrefix,
label,
name,
content
}: { apiPrefix: string; label: string; name: string; content: string } = $props();
let mainContentTextarea: HTMLTextAreaElement;
const updateScrollPercentage = () => {
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
textareaScrollTop.value = (scrollTop / (scrollHeight - clientHeight)) * 100;
};
const handleImagePaste = async (event: ClipboardEvent) => {
const clipboardItems = Array.from(event.clipboardData?.items ?? []);
const file = clipboardItems.find((item) => item.type.startsWith("image/"));
if (!file) return null;
event.preventDefault();
const fileObject = file.getAsFile();
if (!fileObject) return;
const formData = new FormData();
formData.append("file", fileObject);
const request = await fetch("?/pasteImage", {
method: "POST",
body: formData
});
const result = deserialize(await request.clone().text());
applyAction(result);
const response = await request.json();
if (JSON.parse(response.data)[1]) {
const fileId = JSON.parse(response.data)[4];
const fileUrl = `${apiPrefix}/rpc/retrieve_file?id=${fileId}`;
const target = event.target as HTMLTextAreaElement;
const newContent =
target.value.slice(0, target.selectionStart) +
`![](${fileUrl})` +
target.value.slice(target.selectionStart);
previewContent.value = newContent;
} else {
return;
}
};
</script>
<label>
{label}:
<textarea
{name}
rows="20"
bind:value={previewContent.value}
bind:this={mainContentTextarea}
onscroll={updateScrollPercentage}
onpaste={handleImagePaste}
required>{content}</textarea
>
</label>

View File

@@ -2,30 +2,27 @@
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { md } from "$lib/utils"; import { md } from "$lib/utils";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { previewContent, textareaScrollTop } from "$lib/runes.svelte";
const { const {
id, id,
contentType, contentType,
title, title,
children, children,
fullPreview = false, fullPreview = false
previewContent,
previewScrollTop = 0
}: { }: {
id: string; id: string;
contentType: string; contentType: string;
title: string; title: string;
children: Snippet; children: Snippet;
fullPreview?: boolean; fullPreview?: boolean;
previewContent: string;
previewScrollTop?: number;
} = $props(); } = $props();
let previewElement: HTMLDivElement; let previewElement: HTMLDivElement;
$effect(() => { $effect(() => {
const scrollHeight = previewElement.scrollHeight - previewElement.clientHeight; const scrollHeight = previewElement.scrollHeight - previewElement.clientHeight;
previewElement.scrollTop = (previewScrollTop / 100) * scrollHeight; previewElement.scrollTop = (textareaScrollTop.value / 100) * scrollHeight;
}); });
</script> </script>
@@ -66,9 +63,9 @@
<div class="preview" bind:this={previewElement}> <div class="preview" bind:this={previewElement}>
{#if fullPreview} {#if fullPreview}
<iframe src={previewContent} title="Preview"></iframe> <iframe src={previewContent.value} title="Preview"></iframe>
{:else} {:else}
{@html md(previewContent, Object.keys($page.params).length > 1 ? true : false)} {@html md(previewContent.value, Object.keys($page.params).length > 1 ? true : false)}
{/if} {/if}
</div> </div>

View File

@@ -387,16 +387,20 @@ const media = {
// Table settings // Table settings
export interface Settings { export interface Settings {
website_id: string; website_id: string;
accent_color_light_theme: string;
accent_color_dark_theme: string; accent_color_dark_theme: string;
accent_color_light_theme: string;
background_color_dark_theme: string;
background_color_light_theme: string;
favicon_image: string | null; favicon_image: string | null;
last_modified_at: Date; last_modified_at: Date;
last_modified_by: string | null; last_modified_by: string | null;
} }
export interface SettingsInput { export interface SettingsInput {
website_id: string; website_id: string;
accent_color_light_theme?: string;
accent_color_dark_theme?: string; accent_color_dark_theme?: string;
accent_color_light_theme?: string;
background_color_dark_theme?: string;
background_color_light_theme?: string;
favicon_image?: string | null; favicon_image?: string | null;
last_modified_at?: Date; last_modified_at?: Date;
last_modified_by?: string | null; last_modified_by?: string | null;
@@ -405,8 +409,10 @@ const settings = {
tableName: "settings", tableName: "settings",
columns: [ columns: [
"website_id", "website_id",
"accent_color_light_theme",
"accent_color_dark_theme", "accent_color_dark_theme",
"accent_color_light_theme",
"background_color_dark_theme",
"background_color_light_theme",
"favicon_image", "favicon_image",
"last_modified_at", "last_modified_at",
"last_modified_by" "last_modified_by"

View File

@@ -0,0 +1,30 @@
let sendingState = $state(false);
let previewContentState = $state("");
let textareaScrollTopState = $state(0);
export const sending = {
get value() {
return sendingState;
},
set value(val) {
sendingState = val;
}
};
export const previewContent = {
get value() {
return previewContentState;
},
set value(val) {
previewContentState = val;
}
};
export const textareaScrollTop = {
get value() {
return textareaScrollTopState;
},
set value(val) {
textareaScrollTopState = val;
}
};

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { WebsiteOverview } from "../../utils"; import { type WebsiteOverview, md } from "../../utils";
const { const {
websiteOverview, websiteOverview,
@@ -10,7 +10,7 @@
<footer> <footer>
<div class="container"> <div class="container">
<small> <small>
{@html websiteOverview.footer.additional_text.replace( {@html md(websiteOverview.footer.additional_text, false).replace(
"!!legal", "!!legal",
`<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>` `<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>`
)} )}

View File

@@ -15,6 +15,8 @@ import type {
LegalInformation, LegalInformation,
DomainPrefix DomainPrefix
} from "$lib/db-schema"; } from "$lib/db-schema";
import type { SubmitFunction } from "@sveltejs/kit";
import { sending } from "./runes.svelte";
export const ALLOWED_MIME_TYPES = [ export const ALLOWED_MIME_TYPES = [
"image/jpeg", "image/jpeg",
@@ -151,45 +153,59 @@ export const md = (markdownContent: string, showToc = true) => {
return html; return html;
}; };
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => { export const LOADING_DELAY = 500;
const clipboardItems = Array.from(event.clipboardData?.items ?? []); let loadingDelay: number;
const file = clipboardItems.find((item) => item.type.startsWith("image/"));
if (!file) return null; export const enhanceForm = (options?: {
reset?: boolean;
closeModal?: boolean;
}): SubmitFunction => {
return () => {
loadingDelay = window.setTimeout(() => (sending.value = true), LOADING_DELAY);
event.preventDefault(); return async ({ update }) => {
await update({ reset: options?.reset ?? true });
const fileObject = file.getAsFile(); window.clearTimeout(loadingDelay);
if (options?.closeModal) {
if (!fileObject) return; window.location.hash = "!";
const formData = new FormData();
formData.append("file", fileObject);
const request = await fetch("?/pasteImage", {
method: "POST",
body: formData
});
const result = deserialize(await request.clone().text());
applyAction(result);
const response = await request.json();
if (JSON.parse(response.data)[1]) {
const fileId = JSON.parse(response.data)[4];
const fileUrl = `${API_BASE_PREFIX}/rpc/retrieve_file?id=${fileId}`;
const target = event.target as HTMLTextAreaElement;
const newContent =
target.value.slice(0, target.selectionStart) +
`![](${fileUrl})` +
target.value.slice(target.selectionStart);
return newContent;
} else {
return "";
} }
sending.value = false;
};
};
};
export const hexToHSL = (hex: string) => {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
let h = 0;
const l = (max + min) / 2;
const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
if (d !== 0) {
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
break;
case g:
h = ((b - r) / d + 2) / 6;
break;
case b:
h = ((r - g) / d + 4) / 6;
break;
}
}
return {
h: Math.round(h * 360),
s: Math.round(s * 100),
l: Math.round(l * 100)
};
}; };
export interface WebsiteOverview extends Website { export interface WebsiteOverview extends Website {

View File

@@ -3,30 +3,19 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import type { ActionData } from "./$types"; import type { ActionData } from "./$types";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import { sending } from "$lib/runes.svelte";
import { enhanceForm } from "$lib/utils";
const { form }: { form: ActionData } = $props(); const { form }: { form: ActionData } = $props();
let sending = $state(false);
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
<form <form method="POST" use:enhance={enhanceForm()}>
method="POST"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
sending = false;
};
}}
>
<label> <label>
Username: Username:
<input type="text" name="username" required /> <input type="text" name="username" required />

View File

@@ -3,16 +3,15 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import { sending } from "$lib/runes.svelte";
import { enhanceForm } from "$lib/utils";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let sending = $state(false);
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -35,17 +34,7 @@
Account registration is disabled on this instance Account registration is disabled on this instance
</p> </p>
{:else} {:else}
<form <form method="POST" use:enhance={enhanceForm()}>
method="POST"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
sending = false;
};
}}
>
<label> <label>
Username: Username:
<input type="text" name="username" minlength="3" maxlength="16" required /> <input type="text" name="username" minlength="3" maxlength="16" required />

View File

@@ -6,16 +6,15 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import { enhanceForm } from "$lib/utils";
import { sending } from "$lib/runes.svelte";
const { form, data }: { form: ActionData; data: PageServerData } = $props(); const { form, data }: { form: ActionData; data: PageServerData } = $props();
let sending = $state(false);
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -27,19 +26,7 @@
<Modal id="create-website" text="Create website"> <Modal id="create-website" text="Create website">
<h3>Create website</h3> <h3>Create website</h3>
<form <form method="POST" action="?/createWebsite" use:enhance={enhanceForm({ closeModal: true })}>
method="POST"
action="?/createWebsite"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
>
<label> <label>
Type: Type:
<select name="content-type"> <select name="content-type">
@@ -121,15 +108,7 @@
<form <form
method="POST" method="POST"
action="?/updateWebsite" action="?/updateWebsite"
use:enhance={() => { use:enhance={enhanceForm({ reset: false, closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<input type="hidden" name="id" value={id} /> <input type="hidden" name="id" value={id} />
<label> <label>
@@ -157,15 +136,7 @@
<form <form
method="POST" method="POST"
action="?/deleteWebsite" action="?/deleteWebsite"
use:enhance={() => { use:enhance={enhanceForm({ closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<input type="hidden" name="id" value={id} /> <input type="hidden" name="id" value={id} />

View File

@@ -4,16 +4,15 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import { enhanceForm } from "$lib/utils";
import { sending } from "$lib/runes.svelte";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let sending = $state(false);
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -39,18 +38,7 @@
<a href="#logout">Logout</a> <a href="#logout">Logout</a>
</h2> </h2>
<form <form method="POST" action="?/logout" use:enhance={enhanceForm()}>
method="POST"
action="?/logout"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
sending = false;
};
}}
>
<button type="submit">Logout</button> <button type="submit">Logout</button>
</form> </form>
</section> </section>
@@ -68,19 +56,7 @@
Deleting your account will irretrievably erase all data. Deleting your account will irretrievably erase all data.
</p> </p>
<form <form method="POST" action="?/deleteAccount" use:enhance={enhanceForm({ closeModal: true })}>
method="POST"
action="?/deleteAccount"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
>
<label> <label>
Password: Password:
<input type="password" name="password" required /> <input type="password" name="password" required />

View File

@@ -76,8 +76,10 @@ export const actions: Actions = {
"PATCH", "PATCH",
{ {
body: { body: {
accent_color_light_theme: data.get("accent-color-light"),
accent_color_dark_theme: data.get("accent-color-dark"), accent_color_dark_theme: data.get("accent-color-dark"),
accent_color_light_theme: data.get("accent-color-light"),
background_color_dark_theme: data.get("background-color-dark"),
background_color_light_theme: data.get("background-color-light"),
favicon_image: uploadedImage.data?.file_id favicon_image: uploadedImage.data?.file_id
}, },
successMessage: "Successfully updated global settings" successMessage: "Successfully updated global settings"

View File

@@ -1,38 +1,24 @@
<script lang="ts"> <script lang="ts">
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import WebsiteEditor from "$lib/components/WebsiteEditor.svelte"; import WebsiteEditor from "$lib/components/WebsiteEditor.svelte";
import { ALLOWED_MIME_TYPES, handleImagePaste } from "$lib/utils"; import { ALLOWED_MIME_TYPES } from "$lib/utils";
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import type { ActionData, LayoutServerData, PageServerData } from "./$types"; import type { ActionData, LayoutServerData, PageServerData } from "./$types";
import Modal from "$lib/components/Modal.svelte"; import Modal from "$lib/components/Modal.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import { enhanceForm } from "$lib/utils";
import { sending } from "$lib/runes.svelte";
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
import { previewContent } from "$lib/runes.svelte";
const { data, form }: { data: PageServerData & LayoutServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData & LayoutServerData; form: ActionData } = $props();
let previewContent = $state(data.home.main_content); previewContent.value = data.home.main_content;
let mainContentTextarea: HTMLTextAreaElement;
let textareaScrollTop = $state(0);
const updateScrollPercentage = () => {
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
textareaScrollTop = (scrollTop / (scrollHeight - clientHeight)) * 100;
};
const handlePaste = async (event: ClipboardEvent) => {
const newContent = await handleImagePaste(event, data.API_BASE_PREFIX);
if (newContent) {
previewContent = newContent;
}
};
let sending = $state(false);
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -40,9 +26,6 @@
id={data.website.id} id={data.website.id}
contentType={data.website.content_type} contentType={data.website.content_type}
title={data.website.title} title={data.website.title}
previewContent={previewContent ||
"Put some markdown content in main content to see a live preview here"}
previewScrollTop={textareaScrollTop}
> >
<section id="global"> <section id="global">
<h2> <h2>
@@ -52,27 +35,30 @@
action="?/updateGlobal" action="?/updateGlobal"
method="POST" method="POST"
enctype="multipart/form-data" enctype="multipart/form-data"
use:enhance={() => { use:enhance={enhanceForm({ reset: false })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
sending = false;
};
}}
> >
<label> <label>
Light accent color: Background color dark theme:
<input <input
type="color" type="color"
name="accent-color-light" name="background-color-dark"
value={data.globalSettings.accent_color_light_theme} value={data.globalSettings.background_color_dark_theme}
pattern="\S(.*\S)?" pattern="\S(.*\S)?"
required required
/> />
</label> </label>
<label> <label>
Dark accent color: Background color light theme:
<input
type="color"
name="background-color-light"
value={data.globalSettings.background_color_light_theme}
pattern="\S(.*\S)?"
required
/>
</label>
<label>
Accent color dark theme:
<input <input
type="color" type="color"
name="accent-color-dark" name="accent-color-dark"
@@ -81,6 +67,16 @@
required required
/> />
</label> </label>
<label>
Accent color light theme:
<input
type="color"
name="accent-color-light"
value={data.globalSettings.accent_color_light_theme}
pattern="\S(.*\S)?"
required
/>
</label>
<div class="file-field"> <div class="file-field">
<label> <label>
Favicon: Favicon:
@@ -109,14 +105,7 @@
action="?/updateHeader" action="?/updateHeader"
method="POST" method="POST"
enctype="multipart/form-data" enctype="multipart/form-data"
use:enhance={() => { use:enhance={enhanceForm({ reset: false })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
sending = false;
};
}}
> >
<label> <label>
Logo type: Logo type:
@@ -159,30 +148,13 @@
<a href="#home">Home</a> <a href="#home">Home</a>
</h2> </h2>
<form <form action="?/updateHome" method="POST" use:enhance={enhanceForm({ reset: false })}>
action="?/updateHome" <MarkdownEditor
method="POST" apiPrefix={data.API_BASE_PREFIX}
use:enhance={() => { label="Main content"
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
sending = false;
};
}}
>
<label>
Main content:
<textarea
name="main-content" name="main-content"
rows="20" content={data.home.main_content}
bind:value={previewContent} />
bind:this={mainContentTextarea}
onscroll={updateScrollPercentage}
onpaste={handlePaste}
required>{data.home.main_content}</textarea
>
</label>
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
@@ -193,18 +165,7 @@
<a href="#footer">Footer</a> <a href="#footer">Footer</a>
</h2> </h2>
<form <form action="?/updateFooter" method="POST" use:enhance={enhanceForm({ reset: false })}>
action="?/updateFooter"
method="POST"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
sending = false;
};
}}
>
<label> <label>
Additional text: Additional text:
<textarea name="additional-text" rows="5" maxlength="250" required <textarea name="additional-text" rows="5" maxlength="250" required

View File

@@ -6,16 +6,18 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import { enhanceForm } from "$lib/utils";
import { sending } from "$lib/runes.svelte";
import { previewContent } from "$lib/runes.svelte";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let sending = $state(false); previewContent.value = data.home.main_content;
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -23,7 +25,6 @@
id={data.website.id} id={data.website.id}
contentType={data.website.content_type} contentType={data.website.content_type}
title={data.website.title} title={data.website.title}
previewContent={data.home.main_content}
> >
<section id="create-article"> <section id="create-article">
<h2> <h2>
@@ -33,19 +34,7 @@
<Modal id="create-article" text="Create article"> <Modal id="create-article" text="Create article">
<h3>Create article</h3> <h3>Create article</h3>
<form <form method="POST" action="?/createArticle" use:enhance={enhanceForm({ closeModal: true })}>
method="POST"
action="?/createArticle"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
>
<label> <label>
Title: Title:
<input type="text" name="title" pattern="\S(.*\S)?" maxlength="100" required /> <input type="text" name="title" pattern="\S(.*\S)?" maxlength="100" required />
@@ -136,15 +125,7 @@
<form <form
method="POST" method="POST"
action="?/deleteArticle" action="?/deleteArticle"
use:enhance={() => { use:enhance={enhanceForm({ closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<input type="hidden" name="id" value={id} /> <input type="hidden" name="id" value={id} />

View File

@@ -6,34 +6,19 @@
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import Modal from "$lib/components/Modal.svelte"; import Modal from "$lib/components/Modal.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import { handleImagePaste } from "$lib/utils"; import { enhanceForm } from "$lib/utils";
import { sending } from "$lib/runes.svelte";
import { previewContent } from "$lib/runes.svelte";
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let previewContent = $state(data.article.main_content); previewContent.value = data.article?.main_content ?? "";
let mainContentTextarea: HTMLTextAreaElement;
let textareaScrollTop = $state(0);
const updateScrollPercentage = () => {
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
textareaScrollTop = (scrollTop / (scrollHeight - clientHeight)) * 100;
};
const handlePaste = async (event: ClipboardEvent) => {
const newContent = await handleImagePaste(event, data.API_BASE_PREFIX);
if (newContent) {
previewContent = newContent;
}
};
let sending = $state(false);
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -41,9 +26,6 @@
id={data.website.id} id={data.website.id}
contentType={data.website.content_type} contentType={data.website.content_type}
title={data.website.title} title={data.website.title}
previewContent={previewContent ||
"Put some markdown content in main content to see a live preview here"}
previewScrollTop={textareaScrollTop}
> >
<section id="edit-article"> <section id="edit-article">
<h2> <h2>
@@ -54,14 +36,7 @@
method="POST" method="POST"
action="?/editArticle" action="?/editArticle"
enctype="multipart/form-data" enctype="multipart/form-data"
use:enhance={() => { use:enhance={enhanceForm({ reset: false })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
sending = false;
};
}}
> >
{#if data.website.content_type === "Docs"} {#if data.website.content_type === "Docs"}
<label> <label>
@@ -135,18 +110,12 @@
</div> </div>
{/if} {/if}
<label> <MarkdownEditor
Main content: apiPrefix={data.API_BASE_PREFIX}
<textarea label="Main content"
name="main-content" name="main-content"
rows="20" content={data.article.main_content ?? ""}
bind:value={previewContent} />
bind:this={mainContentTextarea}
onscroll={updateScrollPercentage}
onpaste={handlePaste}
required>{data.article.main_content}</textarea
>
</label>
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>

View File

@@ -5,16 +5,18 @@
import Modal from "$lib/components/Modal.svelte"; import Modal from "$lib/components/Modal.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import { enhanceForm } from "$lib/utils";
import { sending } from "$lib/runes.svelte";
import { previewContent } from "$lib/runes.svelte";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let sending = $state(false); previewContent.value = data.home.main_content;
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -22,7 +24,6 @@
id={data.website.id} id={data.website.id}
contentType={data.website.content_type} contentType={data.website.content_type}
title={data.website.title} title={data.website.title}
previewContent={data.home.main_content}
> >
<section id="create-category"> <section id="create-category">
<h2> <h2>
@@ -32,19 +33,7 @@
<Modal id="create-category" text="Create category"> <Modal id="create-category" text="Create category">
<h3>Create category</h3> <h3>Create category</h3>
<form <form method="POST" action="?/createCategory" use:enhance={enhanceForm({ closeModal: true })}>
method="POST"
action="?/createCategory"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
>
<label> <label>
Name: Name:
<input type="text" name="category-name" maxlength="50" required /> <input type="text" name="category-name" maxlength="50" required />
@@ -80,15 +69,7 @@
<form <form
method="POST" method="POST"
action="?/updateCategory" action="?/updateCategory"
use:enhance={() => { use:enhance={enhanceForm({ reset: false, closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<input type="hidden" name="category-id" value={id} /> <input type="hidden" name="category-id" value={id} />
@@ -119,15 +100,7 @@
<form <form
method="POST" method="POST"
action="?/deleteCategory" action="?/deleteCategory"
use:enhance={() => { use:enhance={enhanceForm({ closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<input type="hidden" name="category-id" value={id} /> <input type="hidden" name="category-id" value={id} />

View File

@@ -4,17 +4,18 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import Modal from "$lib/components/Modal.svelte"; import Modal from "$lib/components/Modal.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import { enhanceForm } from "$lib/utils";
import { previewContent, sending } from "$lib/runes.svelte";
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let sending = $state(false); previewContent.value = data.home.main_content;
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -22,7 +23,6 @@
id={data.website.id} id={data.website.id}
contentType={data.website.content_type} contentType={data.website.content_type}
title={data.website.title} title={data.website.title}
previewContent={data.home.main_content}
> >
<section id="add-collaborator"> <section id="add-collaborator">
<h2> <h2>
@@ -35,15 +35,7 @@
<form <form
method="POST" method="POST"
action="?/addCollaborator" action="?/addCollaborator"
use:enhance={() => { use:enhance={enhanceForm({ closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<label> <label>
Username: Username:
@@ -84,15 +76,7 @@
<form <form
method="POST" method="POST"
action="?/updateCollaborator" action="?/updateCollaborator"
use:enhance={() => { use:enhance={enhanceForm({ reset: false, closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<input type="hidden" name="user-id" value={user_id} /> <input type="hidden" name="user-id" value={user_id} />
@@ -116,15 +100,7 @@
<form <form
method="POST" method="POST"
action="?/removeCollaborator" action="?/removeCollaborator"
use:enhance={() => { use:enhance={enhanceForm({ closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<input type="hidden" name="user-id" value={user_id} /> <input type="hidden" name="user-id" value={user_id} />

View File

@@ -23,7 +23,8 @@ export const load: PageServerLoad = async ({ parent, fetch, params }) => {
return { return {
legalInformation, legalInformation,
website website,
API_BASE_PREFIX
}; };
}; };

View File

@@ -4,26 +4,19 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte"; import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import Modal from "$lib/components/Modal.svelte"; import Modal from "$lib/components/Modal.svelte";
import { enhanceForm } from "$lib/utils";
import { sending, previewContent } from "$lib/runes.svelte";
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import MarkdownEditor from "$lib/components/MarkdownEditor.svelte";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let previewContent = $state(data.legalInformation?.main_content); previewContent.value = data.legalInformation?.main_content ?? "";
let mainContentTextarea: HTMLTextAreaElement;
let textareaScrollTop = $state(0);
const updateScrollPercentage = () => {
const { scrollTop, scrollHeight, clientHeight } = mainContentTextarea;
textareaScrollTop = (scrollTop / (scrollHeight - clientHeight)) * 100;
};
let sending = $state(false);
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -31,9 +24,6 @@
id={data.website.id} id={data.website.id}
contentType={data.website.content_type} contentType={data.website.content_type}
title={data.website.title} title={data.website.title}
previewContent={previewContent ||
"Put some markdown content in main content to see a live preview here"}
previewScrollTop={textareaScrollTop}
> >
<section id="legal-information"> <section id="legal-information">
<h2> <h2>
@@ -62,29 +52,14 @@
<form <form
method="POST" method="POST"
action="?/createUpdateLegalInformation" action="?/createUpdateLegalInformation"
use:enhance={() => { use:enhance={enhanceForm({ reset: false })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
sending = false;
};
}}
> >
<label> <MarkdownEditor
Main content: apiPrefix={data.API_BASE_PREFIX}
<textarea label="Main content"
name="main-content" name="main-content"
rows="20" content={data.legalInformation?.main_content ?? ""}
placeholder="## Impressum />
## Privacy policy"
bind:value={previewContent}
bind:this={mainContentTextarea}
onscroll={updateScrollPercentage}
required>{data.legalInformation?.main_content ?? ""}</textarea
>
</label>
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
@@ -94,15 +69,7 @@
<form <form
action="?/deleteLegalInformation" action="?/deleteLegalInformation"
method="post" method="post"
use:enhance={() => { use:enhance={enhanceForm({ closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<h3>Delete legal information</h3> <h3>Delete legal information</h3>
<p> <p>

View File

@@ -6,6 +6,8 @@
import diff from "fast-diff"; import diff from "fast-diff";
import { page } from "$app/stores"; import { page } from "$app/stores";
import { tables } from "$lib/db-schema"; import { tables } from "$lib/db-schema";
import { previewContent } from "$lib/runes.svelte";
import { sanitize } from "isomorphic-dompurify";
const { data }: { data: PageServerData } = $props(); const { data }: { data: PageServerData } = $props();
@@ -45,6 +47,8 @@
resources = restTables; resources = restTables;
} }
previewContent.value = data.home.main_content;
let logsSection: HTMLElement; let logsSection: HTMLElement;
</script> </script>
@@ -52,7 +56,6 @@
id={data.website.id} id={data.website.id}
contentType={data.website.content_type} contentType={data.website.content_type}
title={data.website.title} title={data.website.title}
previewContent={data.home.main_content}
> >
<section id="logs" bind:this={logsSection}> <section id="logs" bind:this={logsSection}>
<hgroup> <hgroup>
@@ -153,7 +156,9 @@
<p>{table_name} &mdash; {operation}</p> <p>{table_name} &mdash; {operation}</p>
</hgroup> </hgroup>
<pre style="white-space: pre-wrap">{@html htmlDiff(oldValue, newValue)}</pre> <pre style="white-space: pre-wrap">{@html sanitize(htmlDiff(oldValue, newValue), {
ALLOWED_TAGS: ["ins", "del"]
})}</pre>
</Modal> </Modal>
</td> </td>
</tr> </tr>

View File

@@ -1,14 +1,14 @@
import { readFile, mkdir, writeFile, rename } from "node:fs/promises";
import { join } from "node:path";
import { type WebsiteOverview, slugify } from "$lib/utils";
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
import { render } from "svelte/server";
import BlogIndex from "$lib/templates/blog/BlogIndex.svelte";
import BlogArticle from "$lib/templates/blog/BlogArticle.svelte";
import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
import { dev } from "$app/environment"; import { dev } from "$app/environment";
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
import BlogArticle from "$lib/templates/blog/BlogArticle.svelte";
import BlogIndex from "$lib/templates/blog/BlogIndex.svelte";
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
import { type WebsiteOverview, hexToHSL, slugify } from "$lib/utils";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { render } from "svelte/server";
import type { Actions, PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ params, fetch }) => { export const load: PageServerLoad = async ({ params, fetch }) => {
const websiteOverview: WebsiteOverview = ( const websiteOverview: WebsiteOverview = (
@@ -169,8 +169,6 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview = tru
</html>`; </html>`;
}; };
console.log(websiteData);
const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, { const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
props: { props: {
websiteOverview: websiteData, websiteOverview: websiteData,
@@ -235,21 +233,35 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview = tru
encoding: "utf-8" encoding: "utf-8"
} }
); );
const {
h: hDark,
s: sDark,
l: lDark
} = hexToHSL(websiteData.settings.background_color_dark_theme);
const {
h: hLight,
s: sLight,
l: lLight
} = hexToHSL(websiteData.settings.background_color_light_theme);
await writeFile( await writeFile(
join(uploadDir, "styles.css"), join(uploadDir, "styles.css"),
commonStyles commonStyles
.concat(specificStyles) .concat(specificStyles)
.replace(/(?<=\/\* BACKGROUND_COLOR_DARK_THEME_H \*\/\s*).*(?=;)/, ` ${hDark}`)
.replace(/(?<=\/\* BACKGROUND_COLOR_DARK_THEME_S \*\/\s*).*(?=;)/, ` ${sDark}%`)
.replace(/(?<=\/\* BACKGROUND_COLOR_DARK_THEME_L \*\/\s*).*(?=;)/, ` ${lDark}%`)
.replace(/(?<=\/\* BACKGROUND_COLOR_LIGHT_THEME_H \*\/\s*).*(?=;)/, ` ${hLight}`)
.replace(/(?<=\/\* BACKGROUND_COLOR_LIGHT_THEME_S \*\/\s*).*(?=;)/, ` ${sLight}%`)
.replace(/(?<=\/\* BACKGROUND_COLOR_LIGHT_THEME_L \*\/\s*).*(?=;)/, ` ${lLight}%`)
.replace( .replace(
/--color-accent:\s*(.*?);/, /(?<=\/\* ACCENT_COLOR_DARK_THEME \*\/\s*).*(?=;)/,
`--color-accent: ${websiteData.settings.accent_color_dark_theme};` ` ${websiteData.settings.accent_color_dark_theme}`
) )
.replace( .replace(
/@media\s*\(prefers-color-scheme:\s*dark\)\s*{[^}]*--color-accent:\s*(.*?);/, /(?<=\/\* ACCENT_COLOR_LIGHT_THEME \*\/\s*).*(?=;)/,
(match) => ` ${websiteData.settings.accent_color_light_theme}`
match.replace(
/--color-accent:\s*(.*?);/,
`--color-accent: ${websiteData.settings.accent_color_light_theme};`
)
) )
); );
}; };

View File

@@ -5,16 +5,18 @@
import type { ActionData, PageServerData } from "./$types"; import type { ActionData, PageServerData } from "./$types";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import Modal from "$lib/components/Modal.svelte"; import Modal from "$lib/components/Modal.svelte";
import { enhanceForm } from "$lib/utils";
import { sending } from "$lib/runes.svelte";
import { previewContent } from "$lib/runes.svelte";
const { data, form }: { data: PageServerData; form: ActionData } = $props(); const { data, form }: { data: PageServerData; form: ActionData } = $props();
let sending = $state(false); previewContent.value = data.websitePreviewUrl;
let loadingDelay: number;
</script> </script>
<SuccessOrError success={form?.success} message={form?.message} /> <SuccessOrError success={form?.success} message={form?.message} />
{#if sending} {#if sending.value}
<LoadingSpinner /> <LoadingSpinner />
{/if} {/if}
@@ -22,7 +24,6 @@
id={data.websiteOverview.id} id={data.websiteOverview.id}
contentType={data.websiteOverview.content_type} contentType={data.websiteOverview.content_type}
title={data.websiteOverview.title} title={data.websiteOverview.title}
previewContent={data.websitePreviewUrl}
fullPreview={true} fullPreview={true}
> >
<section id="publish-website"> <section id="publish-website">
@@ -34,18 +35,7 @@
is published. If you are happy with the results, click the button below and your website will is published. If you are happy with the results, click the button below and your website will
be published on the Internet. be published on the Internet.
</p> </p>
<form <form method="POST" action="?/publishWebsite" use:enhance={enhanceForm()}>
method="POST"
action="?/publishWebsite"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
sending = false;
};
}}
>
<button type="submit">Publish</button> <button type="submit">Publish</button>
</form> </form>
</section> </section>
@@ -69,14 +59,7 @@
<form <form
method="POST" method="POST"
action="?/createUpdateCustomDomainPrefix" action="?/createUpdateCustomDomainPrefix"
use:enhance={() => { use:enhance={enhanceForm({ reset: false })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
sending = false;
};
}}
> >
<label> <label>
Prefix: Prefix:
@@ -98,15 +81,7 @@
<form <form
action="?/deleteCustomDomainPrefix" action="?/deleteCustomDomainPrefix"
method="post" method="post"
use:enhance={() => { use:enhance={enhanceForm({ closeModal: true })}
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
> >
<h3>Delete domain prefix</h3> <h3>Delete domain prefix</h3>
<p> <p>

View File

@@ -5,6 +5,7 @@
import type { Snippet } from "svelte"; import type { Snippet } from "svelte";
import { navigating } from "$app/stores"; import { navigating } from "$app/stores";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte"; import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import { LOADING_DELAY } from "$lib/utils";
const { data, children }: { data: LayoutServerData; children: Snippet } = $props(); const { data, children }: { data: LayoutServerData; children: Snippet } = $props();
@@ -20,7 +21,7 @@
$effect(() => { $effect(() => {
if ($navigating && ["link", "goto"].includes($navigating.type)) { if ($navigating && ["link", "goto"].includes($navigating.type)) {
loadingDelay = window.setTimeout(() => (loading = true), 500); loadingDelay = window.setTimeout(() => (loading = true), LOADING_DELAY);
} else { } else {
window.clearTimeout(loadingDelay); window.clearTimeout(loadingDelay);
loading = false; loading = false;

View File

@@ -18,10 +18,10 @@ const config = {
"default-src": ["none"], "default-src": ["none"],
"script-src": ["self"], "script-src": ["self"],
"style-src": ["self", "https:", "unsafe-inline"], "style-src": ["self", "https:", "unsafe-inline"],
"img-src": ["self", "data:", "https:"], "img-src": ["self", "data:", "https:", "http:"],
"font-src": ["self", "https:"], "font-src": ["self", "https:"],
"connect-src": ["self"], "connect-src": ["self"],
"frame-src": ["self", "https:"], "frame-src": ["self", "https:", "http:"],
"object-src": ["none"], "object-src": ["none"],
"base-uri": ["self"], "base-uri": ["self"],
"frame-ancestors": ["none"], "frame-ancestors": ["none"],

View File

@@ -19,17 +19,22 @@
} }
:root { :root {
--bg-primary: white; --bg-primary-h: /* BACKGROUND_COLOR_LIGHT_THEME_H */ 0;
--bg-secondary: hsl(0 0% 95%); --bg-primary-s: /* BACKGROUND_COLOR_LIGHT_THEME_S */ 0%;
--bg-tertiary: hsl(0 0% 90%); --bg-primary-l: /* BACKGROUND_COLOR_LIGHT_THEME_L */ 100%;
--bg-blurred: rgba(255, 255, 255, 0.5); --bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l));
--bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 5%));
--bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 10%));
--bg-blurred: hsla(
var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) - 20%)
);
--color-text: black; --color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 0%);
--color-text-invert: white; --color-text-invert: var(--bg-primary);
--color-border: hsl(0 0% 50%); --color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 50%));
--color-accent: hsl(210, 100%, 30%); --color-accent: /* ACCENT_COLOR_LIGHT_THEME */ hsl(210 100% 30%);
--color-success: hsl(105, 100%, 30%); --color-success: hsl(105 100% 30%);
--color-error: hsl(0, 100%, 30%); --color-error: hsl(0 100% 30%);
--border-primary: 0.0625rem solid var(--color-border); --border-primary: 0.0625rem solid var(--color-border);
--border-radius: 0.125rem; --border-radius: 0.125rem;
@@ -73,16 +78,22 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
:root { :root {
--bg-primary: hsl(0 0% 15%); --bg-primary-h: /* BACKGROUND_COLOR_DARK_THEME_H */ 0;
--bg-secondary: hsl(0 0% 20%); --bg-primary-s: /* BACKGROUND_COLOR_DARK_THEME_S */ 0%;
--bg-tertiary: hsl(0 0% 25%); --bg-primary-l: /* BACKGROUND_COLOR_DARK_THEME_L */ 15%;
--bg-blurred: rgba(0, 0, 0, 0.5); --bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l));
--bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 5%));
--bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 10%));
--bg-blurred: hsla(
var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) + 20%)
);
--color-text: white; --color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 100%);
--color-text-invert: black; --color-text-invert: var(--bg-primary);
--color-accent: hsl(210, 100%, 80%); --color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 50%));
--color-success: hsl(105, 100%, 80%); --color-accent: /* ACCENT_COLOR_DARK_THEME */ hsl(210 100% 80%);
--color-error: hsl(0, 100%, 80%); --color-success: hsl(105 100% 80%);
--color-error: hsl(0 100% 80%);
color-scheme: dark; color-scheme: dark;
} }

View File

@@ -99,8 +99,8 @@ test.describe.serial("Collaborator tests", () => {
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("link", { name: "Legal information" }).click(); await page.getByRole("link", { name: "Legal information" }).click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click(); await page.getByLabel("Main content:").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content"); await page.getByLabel("Main content:").fill("## Content");
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
}); });
@@ -330,8 +330,8 @@ test.describe.serial("Collaborator tests", () => {
test("Create/Update legal information", async ({ page }) => { test("Create/Update legal information", async ({ page }) => {
await page.getByRole("link", { name: "Blog" }).click(); await page.getByRole("link", { name: "Blog" }).click();
await page.getByRole("link", { name: "Legal information" }).click(); await page.getByRole("link", { name: "Legal information" }).click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click(); await page.getByLabel("Main content:").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content"); await page.getByLabel("Main content:").fill("## Content");
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
if (permissionLevel === 30) { if (permissionLevel === 30) {
@@ -340,8 +340,8 @@ test.describe.serial("Collaborator tests", () => {
await expect(page.getByText("Insufficient permissions")).toBeVisible(); await expect(page.getByText("Insufficient permissions")).toBeVisible();
} }
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click(); await page.getByLabel("Main content:").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content updated"); await page.getByLabel("Main content:").fill("## Content updated");
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
if (permissionLevel === 30) { if (permissionLevel === 30) {

View File

@@ -235,13 +235,13 @@ test.describe.serial("Website tests", () => {
test("Create/Update legal information", async ({ authenticatedPage: page }) => { test("Create/Update legal information", async ({ authenticatedPage: page }) => {
await page.getByRole("link", { name: "Blog" }).click(); await page.getByRole("link", { name: "Blog" }).click();
await page.getByRole("link", { name: "Legal information" }).click(); await page.getByRole("link", { name: "Legal information" }).click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click(); await page.getByLabel("Main content:").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content"); await page.getByLabel("Main content:").fill("## Content");
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Successfully created/updated legal")).toBeVisible(); await expect(page.getByText("Successfully created/updated legal")).toBeVisible();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click(); await page.getByLabel("Main content:").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content updated"); await page.getByLabel("Main content:").fill("## Content updated");
await page.getByRole("button", { name: "Submit" }).click(); await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText("Successfully created/updated legal")).toBeVisible(); await expect(page.getByText("Successfully created/updated legal")).toBeVisible();
}); });