mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 19:01:35 +01:00
Refactor web app code and add background color setting
This commit is contained in:
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 { 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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
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">
|
||||
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>`
|
||||
)}
|
||||
|
||||
@@ -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) +
|
||||
`` +
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user