Add legal information operation site

This commit is contained in:
thiloho
2024-09-08 16:42:32 +02:00
parent 9a8a333823
commit 8121be1d96
15 changed files with 542 additions and 8 deletions

View File

@@ -49,6 +49,9 @@
<li>
<a href="/website/{id}/collaborators">Collaborators</a>
</li>
<li>
<a href="/website/{id}/legal-information">Legal information</a>
</li>
<li>
<a href="/website/{id}/publish">Publish</a>
</li>

View File

@@ -50,4 +50,4 @@
</main>
{/if}
<Footer text={footerAdditionalText} />
<Footer text={footerAdditionalText} isIndexPage={false} />

View File

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

View File

@@ -42,4 +42,4 @@
</main>
{/if}
<Footer text={footerAdditionalText} />
<Footer text={footerAdditionalText} isIndexPage={false} />

View File

@@ -150,7 +150,7 @@ export const md = (markdownContent: string, showToc = true) => {
};
export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: string) => {
const clipboardItems = Array.from(event.clipboardData?.items || []);
const clipboardItems = Array.from(event.clipboardData?.items ?? []);
const file = clipboardItems.find((item) => item.type.startsWith("image/"));
if (!file) return null;

View File

@@ -39,7 +39,8 @@
id={data.website.id}
contentType={data.website.content_type}
title={data.website.title}
{previewContent}
previewContent={previewContent ||
"Put some markdown content in main content to see a live preview here"}
previewScrollTop={textareaScrollTop}
>
<section id="global">

View File

@@ -0,0 +1,80 @@
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import { rm } from "node:fs/promises";
import { join } from "node:path";
export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) => {
const legalInformationData = await fetch(
`${API_BASE_PREFIX}/legal_information?website_id=eq.${params.websiteId}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json"
}
}
);
const legalInformation = legalInformationData.ok ? await legalInformationData.json() : null;
const { website } = await parent();
return {
legalInformation,
website
};
};
export const actions: Actions = {
createUpdateLegalInformation: async ({ request, fetch, cookies, params }) => {
const data = await request.formData();
const res = await fetch(`${API_BASE_PREFIX}/legal_information`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`,
Prefer: "resolution=merge-duplicates",
Accept: "application/vnd.pgrst.object+json"
},
body: JSON.stringify({
website_id: params.websiteId,
main_content: data.get("main-content")
})
});
if (!res.ok) {
const response = await res.json();
return { success: false, message: response.message };
}
return {
success: true,
message: `Successfully ${res.status === 201 ? "created" : "updated"} legal information`
};
},
deleteLegalInformation: async ({ fetch, cookies, params }) => {
const res = await fetch(
`${API_BASE_PREFIX}/legal_information?website_id=eq.${params.websiteId}`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`
}
}
);
if (!res.ok) {
const response = await res.json();
return { success: false, message: response.message };
}
await rm(
join("/", "var", "www", "archtika-websites", params.websiteId, "legal-information.html"),
{ force: true }
);
return { success: true, message: `Successfully deleted legal information` };
}
};

View File

@@ -0,0 +1,121 @@
<script lang="ts">
import { enhance } from "$app/forms";
import WebsiteEditor from "$lib/components/WebsiteEditor.svelte";
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import Modal from "$lib/components/Modal.svelte";
import type { ActionData, PageServerData } from "./$types";
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);
</script>
<SuccessOrError success={form?.success} message={form?.message} />
{#if sending}
<LoadingSpinner />
{/if}
<WebsiteEditor
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>
<a href="#legal-information">Legal information</a>
</h2>
<p>
Static websites that do not collect user data and do not use cookies generally have minimal
legal obligations regarding privacy policies, imprints, etc. However, it may still be a good
idea to include, for example:
</p>
<ol>
<li>A simple privacy policy stating that no personal information is collected or stored</li>
<li>
An imprint (if required by local law) with contact information for the site owner/operator
</li>
</ol>
<p>Always consult local laws and regulations for specific requirements in your jurisdiction.</p>
<p>
To include a link to your legal information in the footer, you can write <code>!!legal</code>.
</p>
<form
method="POST"
action="?/createUpdateLegalInformation"
use:enhance={() => {
sending = true;
return async ({ update }) => {
await update({ reset: false });
sending = 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>
<button type="submit">Submit</button>
</form>
{#if data.legalInformation?.main_content}
<Modal id="delete-legal-information" text="Delete">
<form
action="?/deleteLegalInformation"
method="post"
use:enhance={() => {
sending = true;
return async ({ update }) => {
await update();
window.location.hash = "!";
sending = false;
previewContent = null;
};
}}
>
<h3>Delete legal information</h3>
<p>
<strong>Caution!</strong>
This action will remove the legal information page from the website and delete all data.
</p>
<button type="submit">Delete legal information</button>
</form>
</Modal>
{/if}
</section>
</WebsiteEditor>
<style>
form[action="?/createUpdateLegalInformation"] {
margin-block-start: var(--space-s);
}
</style>

View File

@@ -32,6 +32,7 @@ interface WebsiteData {
categorized_articles: {
[key: string]: { title: string; publication_date: string; meta_description: string }[];
};
legal_information_main_content: string | null;
}
export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) => {
@@ -251,6 +252,67 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
await writeFile(join(uploadDir, "articles", `${articleFileName}.html`), articleFileContents);
}
if (websiteData.legal_information_main_content) {
let head = "";
let body = "";
switch (websiteData.content_type) {
case "Blog":
{
({ head, body } = render(BlogIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: "Legal information",
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.legal_information_main_content ?? "", false),
articles: [],
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
case "Docs":
{
({ head, body } = render(DocsIndex, {
props: {
favicon: websiteData.favicon_image
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
: "",
title: "Legal information",
logoType: websiteData.logo_type,
logo:
websiteData.logo_type === "text"
? (websiteData.logo_text ?? "")
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
mainContent: md(websiteData.legal_information_main_content ?? "", false),
categorizedArticles: {},
footerAdditionalText: md(websiteData.additional_text ?? "")
}
}));
}
break;
}
const legalInformationFileContents = `
<!DOCTYPE html>
<html lang="en">
<head>
${head}
</head>
<body>
${body}
</body>
</html>`;
await writeFile(join(uploadDir, "legal-information.html"), legalInformationFileContents);
}
const commonStyles = await readFile(`${process.cwd()}/template-styles/common-styles.css`, {
encoding: "utf-8"
});

View File

@@ -47,7 +47,7 @@
</form>
{#if data.website.is_published}
<section>
<section id="publication-status">
<h3>
<a href="#publication-status">Publication status</a>
</h3>

View File

@@ -97,6 +97,11 @@ test.describe.serial("Collaborator tests", () => {
await page.getByLabel("Username:").click();
await page.getByLabel("Username:").fill(collabUsername);
await page.getByRole("button", { name: "Submit" }).click();
await page.getByRole("link", { name: "Legal information" }).click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content");
await page.getByRole("button", { name: "Submit" }).click();
});
for (const permissionLevel of permissionLevels) {
@@ -322,6 +327,45 @@ test.describe.serial("Collaborator tests", () => {
await expect(page.getByText("You do not have the required")).toBeVisible();
}
});
test("Create/Update legal information", async ({ page }) => {
await page.getByRole("link", { name: "Blog" }).click();
await page.getByRole("link", { name: "Legal information" }).click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content");
await page.getByRole("button", { name: "Submit" }).click();
if (permissionLevel === 30) {
await expect(page.getByText("Successfully created legal")).toBeVisible();
} else {
await expect(page.getByText("You do not have the required")).toBeVisible();
}
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").click();
await page.getByPlaceholder("## Impressum\n\n## Privacy policy").fill("## Content updated");
await page.getByRole("button", { name: "Submit" }).click();
if (permissionLevel === 30) {
await expect(page.getByText("Successfully updated legal")).toBeVisible();
} else {
await expect(page.getByText("You do not have the required")).toBeVisible();
}
});
test("Delete legal information", async ({ page }) => {
await page
.getByRole("link", {
name: [10, 20].includes(permissionLevel) ? "Documentation" : "Blog"
})
.click();
await page.getByRole("link", { name: "Legal information" }).click();
await page.getByRole("button", { name: "Delete" }).click();
await page.getByRole("button", { name: "Delete legal information" }).click();
if (permissionLevel === 30) {
await expect(page.getByText("Successfully deleted legal")).toBeVisible();
} else {
await expect(page.getByText("You do not have the required")).toBeVisible();
}
});
test("Create category", async ({ page }) => {
await page.getByRole("link", { name: "Documentation" }).click();
await page.getByRole("link", { name: "Categories" }).click();

View File

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