mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 02:41:35 +01:00
Refactor web app code and add background color setting
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
72
web-app/src/lib/components/MarkdownEditor.svelte
Normal file
72
web-app/src/lib/components/MarkdownEditor.svelte
Normal 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) +
|
||||||
|
`` +
|
||||||
|
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>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
30
web-app/src/lib/runes.svelte.ts
Normal file
30
web-app/src/lib/runes.svelte.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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>`
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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) +
|
|
||||||
`` +
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export const load: PageServerLoad = async ({ parent, fetch, params }) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
legalInformation,
|
legalInformation,
|
||||||
website
|
website,
|
||||||
|
API_BASE_PREFIX
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} — {operation}</p>
|
<p>{table_name} — {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>
|
||||||
|
|||||||
@@ -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};`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user