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

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

View File

@@ -387,16 +387,20 @@ const media = {
// Table settings
export interface Settings {
website_id: string;
accent_color_light_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;
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;
accent_color_light_theme?: string;
background_color_dark_theme?: string;
background_color_light_theme?: string;
favicon_image?: string | null;
last_modified_at?: Date;
last_modified_by?: string | null;
@@ -405,8 +409,10 @@ const settings = {
tableName: "settings",
columns: [
"website_id",
"accent_color_light_theme",
"accent_color_dark_theme",
"accent_color_light_theme",
"background_color_dark_theme",
"background_color_light_theme",
"favicon_image",
"last_modified_at",
"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">
import type { WebsiteOverview } from "../../utils";
import { type WebsiteOverview, md } from "../../utils";
const {
websiteOverview,
@@ -10,7 +10,7 @@
<footer>
<div class="container">
<small>
{@html websiteOverview.footer.additional_text.replace(
{@html md(websiteOverview.footer.additional_text, false).replace(
"!!legal",
`<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>`
)}

View File

@@ -15,6 +15,8 @@ import type {
LegalInformation,
DomainPrefix
} from "$lib/db-schema";
import type { SubmitFunction } from "@sveltejs/kit";
import { sending } from "./runes.svelte";
export const ALLOWED_MIME_TYPES = [
"image/jpeg",
@@ -151,45 +153,59 @@ export const md = (markdownContent: string, showToc = true) => {
return html;
};
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => {
const clipboardItems = Array.from(event.clipboardData?.items ?? []);
const file = clipboardItems.find((item) => item.type.startsWith("image/"));
export const LOADING_DELAY = 500;
let loadingDelay: number;
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 });
window.clearTimeout(loadingDelay);
if (options?.closeModal) {
window.location.hash = "!";
}
sending.value = false;
};
};
};
const fileObject = file.getAsFile();
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;
if (!fileObject) return;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const d = max - min;
const formData = new FormData();
formData.append("file", fileObject);
let h = 0;
const l = (max + min) / 2;
const s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
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 "";
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 {

View File

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

View File

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

View File

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

View File

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

View File

@@ -76,8 +76,10 @@ export const actions: Actions = {
"PATCH",
{
body: {
accent_color_light_theme: data.get("accent-color-light"),
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
},
successMessage: "Successfully updated global settings"

View File

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

View File

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

View File

@@ -6,34 +6,19 @@
import type { ActionData, PageServerData } from "./$types";
import Modal from "$lib/components/Modal.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();
let previewContent = $state(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;
previewContent.value = data.article?.main_content ?? "";
</script>
<SuccessOrError success={form?.success} message={form?.message} />
{#if sending}
{#if sending.value}
<LoadingSpinner />
{/if}
@@ -41,9 +26,6 @@
id={data.website.id}
contentType={data.website.content_type}
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">
<h2>
@@ -54,14 +36,7 @@
method="POST"
action="?/editArticle"
enctype="multipart/form-data"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update({ reset: false });
window.clearTimeout(loadingDelay);
sending = false;
};
}}
use:enhance={enhanceForm({ reset: false })}
>
{#if data.website.content_type === "Docs"}
<label>
@@ -135,18 +110,12 @@
</div>
{/if}
<label>
Main content:
<textarea
name="main-content"
rows="20"
bind:value={previewContent}
bind:this={mainContentTextarea}
onscroll={updateScrollPercentage}
onpaste={handlePaste}
required>{data.article.main_content}</textarea
>
</label>
<MarkdownEditor
apiPrefix={data.API_BASE_PREFIX}
label="Main content"
name="main-content"
content={data.article.main_content ?? ""}
/>
<button type="submit">Submit</button>
</form>

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,8 @@
import diff from "fast-diff";
import { page } from "$app/stores";
import { tables } from "$lib/db-schema";
import { previewContent } from "$lib/runes.svelte";
import { sanitize } from "isomorphic-dompurify";
const { data }: { data: PageServerData } = $props();
@@ -45,6 +47,8 @@
resources = restTables;
}
previewContent.value = data.home.main_content;
let logsSection: HTMLElement;
</script>
@@ -52,7 +56,6 @@
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
previewContent={data.home.main_content}
>
<section id="logs" bind:this={logsSection}>
<hgroup>
@@ -153,7 +156,9 @@
<p>{table_name} &mdash; {operation}</p>
</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>
</td>
</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 { 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 }) => {
const websiteOverview: WebsiteOverview = (
@@ -169,8 +169,6 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview = tru
</html>`;
};
console.log(websiteData);
const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
props: {
websiteOverview: websiteData,
@@ -235,21 +233,35 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview = tru
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(
join(uploadDir, "styles.css"),
commonStyles
.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(
/--color-accent:\s*(.*?);/,
`--color-accent: ${websiteData.settings.accent_color_dark_theme};`
/(?<=\/\* ACCENT_COLOR_DARK_THEME \*\/\s*).*(?=;)/,
` ${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.settings.accent_color_light_theme};`
)
/(?<=\/\* ACCENT_COLOR_LIGHT_THEME \*\/\s*).*(?=;)/,
` ${websiteData.settings.accent_color_light_theme}`
)
);
};

View File

@@ -5,16 +5,18 @@
import type { ActionData, PageServerData } from "./$types";
import LoadingSpinner from "$lib/components/LoadingSpinner.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();
let sending = $state(false);
let loadingDelay: number;
previewContent.value = data.websitePreviewUrl;
</script>
<SuccessOrError success={form?.success} message={form?.message} />
{#if sending}
{#if sending.value}
<LoadingSpinner />
{/if}
@@ -22,7 +24,6 @@
id={data.websiteOverview.id}
contentType={data.websiteOverview.content_type}
title={data.websiteOverview.title}
previewContent={data.websitePreviewUrl}
fullPreview={true}
>
<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
be published on the Internet.
</p>
<form
method="POST"
action="?/publishWebsite"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
sending = false;
};
}}
>
<form method="POST" action="?/publishWebsite" use:enhance={enhanceForm()}>
<button type="submit">Publish</button>
</form>
</section>
@@ -69,14 +59,7 @@
<form
method="POST"
action="?/createUpdateCustomDomainPrefix"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
sending = false;
};
}}
use:enhance={enhanceForm({ reset: false })}
>
<label>
Prefix:
@@ -98,15 +81,7 @@
<form
action="?/deleteCustomDomainPrefix"
method="post"
use:enhance={() => {
loadingDelay = window.setTimeout(() => (sending = true), 500);
return async ({ update }) => {
await update();
window.clearTimeout(loadingDelay);
window.location.hash = "!";
sending = false;
};
}}
use:enhance={enhanceForm({ closeModal: true })}
>
<h3>Delete domain prefix</h3>
<p>

View File

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