From b0666f4a8c5fdaa15ca517766185b9733aa8cd06 Mon Sep 17 00:00:00 2001 From: Thilo Hohlt <123883702+thiloho@users.noreply.github.com> Date: Thu, 1 Aug 2024 18:09:35 +0200 Subject: [PATCH] Add basic forms and routes --- web-app/src/app.html | 1 + .../src/lib/components/WebsiteEditor.svelte | 45 ++++ web-app/src/lib/server/utils.ts | 66 ++++++ .../routes/(anonymous)/login/+page.server.ts | 2 +- .../src/routes/(anonymous)/login/+page.svelte | 20 +- .../(anonymous)/register/+page.server.ts | 2 +- .../routes/(anonymous)/register/+page.svelte | 20 +- .../routes/(authenticated)/+page.server.ts | 29 ++- .../src/routes/(authenticated)/+page.svelte | 165 +++++++------- .../(authenticated)/account/+page.server.ts | 6 +- .../(authenticated)/account/+page.svelte | 22 +- .../website/[websiteId]/+layout.server.ts | 15 +- .../website/[websiteId]/+page.server.ts | 208 +----------------- .../website/[websiteId]/+page.svelte | 142 +++++++++++- .../[websiteId]/articles/+page.server.ts | 103 +++++++++ .../website/[websiteId]/articles/+page.svelte | 82 +++++++ .../articles/[articleId]/+page.server.ts | 59 +++++ .../articles/[articleId]/+page.svelte | 62 ++++++ web-app/src/routes/+layout.svelte | 53 ++++- web-app/svelte.config.ts | 2 +- 20 files changed, 762 insertions(+), 342 deletions(-) create mode 100644 web-app/src/lib/components/WebsiteEditor.svelte create mode 100644 web-app/src/lib/server/utils.ts create mode 100644 web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts create mode 100644 web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte create mode 100644 web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts create mode 100644 web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte diff --git a/web-app/src/app.html b/web-app/src/app.html index 84ffad1..66aee01 100644 --- a/web-app/src/app.html +++ b/web-app/src/app.html @@ -4,6 +4,7 @@ + %sveltekit.head% diff --git a/web-app/src/lib/components/WebsiteEditor.svelte b/web-app/src/lib/components/WebsiteEditor.svelte new file mode 100644 index 0000000..c767c95 --- /dev/null +++ b/web-app/src/lib/components/WebsiteEditor.svelte @@ -0,0 +1,45 @@ + + +
+

{title}

+ +
+ Settings + Articles +
+ + {@render children()} +
+ +
+ {@html previewContent} +
+ + diff --git a/web-app/src/lib/server/utils.ts b/web-app/src/lib/server/utils.ts new file mode 100644 index 0000000..958b727 --- /dev/null +++ b/web-app/src/lib/server/utils.ts @@ -0,0 +1,66 @@ +import { randomUUID } from "node:crypto"; +import { mkdir, writeFile } from "node:fs/promises"; +import { extname, join, relative } from "node:path"; +import { ALLOWED_MIME_TYPES } from "../utils"; + +export const handleFileUpload = async ( + file: File, + contentId: string, + userId: string, + session_token: string | undefined, + customFetch: typeof fetch +) => { + if (file.size === 0) return undefined; + + const MAX_FILE_SIZE = 1024 * 1024; + + if (file.size > MAX_FILE_SIZE) { + return { + success: false, + message: `File size exceeds the maximum limit of ${MAX_FILE_SIZE / 1024 / 1024} MB.` + }; + } + + if (!ALLOWED_MIME_TYPES.includes(file.type)) { + return { + success: false, + message: "Invalid file type. JPEG, PNG, SVG and WEBP are allowed." + }; + } + + const buffer = Buffer.from(await file.arrayBuffer()); + const uploadDir = join(process.cwd(), "static", "user-uploads", userId); + await mkdir(uploadDir, { recursive: true }); + + const fileId = randomUUID(); + const fileExtension = extname(file.name); + const filepath = join(uploadDir, `${fileId}${fileExtension}`); + + await writeFile(filepath, buffer); + + const relativePath = relative(join(process.cwd(), "static"), filepath); + + const res = await customFetch("http://localhost:3000/media", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${session_token}`, + Prefer: "return=representation", + Accept: "application/vnd.pgrst.object+json" + }, + body: JSON.stringify({ + website_id: contentId, + user_id: userId, + original_name: file.name, + file_system_path: relativePath + }) + }); + + const response = await res.json(); + + if (!res.ok) { + return { success: false, message: response.message }; + } + + return { success: true, content: response.id }; +}; diff --git a/web-app/src/routes/(anonymous)/login/+page.server.ts b/web-app/src/routes/(anonymous)/login/+page.server.ts index bcab662..2b58918 100644 --- a/web-app/src/routes/(anonymous)/login/+page.server.ts +++ b/web-app/src/routes/(anonymous)/login/+page.server.ts @@ -18,6 +18,6 @@ export const actions = { } cookies.set("session_token", response.token, { path: "/" }); - return { success: true }; + return { success: true, message: "Successfully logged in" }; } }; diff --git a/web-app/src/routes/(anonymous)/login/+page.svelte b/web-app/src/routes/(anonymous)/login/+page.svelte index f086a8b..0ba5c3a 100644 --- a/web-app/src/routes/(anonymous)/login/+page.svelte +++ b/web-app/src/routes/(anonymous)/login/+page.svelte @@ -4,21 +4,21 @@ const { form } = $props(); +{#if form?.success} +

{form.message}

+{/if} + +{#if form?.success === false} +

{form.message}

+{/if} +
- {#if form?.success} -

Successfully logged in

- {/if} - - {#if form?.success === false} -

{form.message}

- {/if} - diff --git a/web-app/src/routes/(anonymous)/register/+page.server.ts b/web-app/src/routes/(anonymous)/register/+page.server.ts index d7656e6..b666dca 100644 --- a/web-app/src/routes/(anonymous)/register/+page.server.ts +++ b/web-app/src/routes/(anonymous)/register/+page.server.ts @@ -17,6 +17,6 @@ export const actions = { return { success: false, message: response.message }; } - return { success: true }; + return { success: true, message: "Successfully registered, you can now login" }; } }; diff --git a/web-app/src/routes/(anonymous)/register/+page.svelte b/web-app/src/routes/(anonymous)/register/+page.svelte index b365c94..f320b40 100644 --- a/web-app/src/routes/(anonymous)/register/+page.svelte +++ b/web-app/src/routes/(anonymous)/register/+page.svelte @@ -4,21 +4,21 @@ const { form } = $props(); +{#if form?.success} +

{form.message}

+{/if} + +{#if form?.success === false} +

{form.message}

+{/if} + - {#if form?.success} -

Successfully registered

- {/if} - - {#if form?.success === false} -

{form.message}

- {/if} - diff --git a/web-app/src/routes/(authenticated)/+page.server.ts b/web-app/src/routes/(authenticated)/+page.server.ts index 0e9043d..f9878ba 100644 --- a/web-app/src/routes/(authenticated)/+page.server.ts +++ b/web-app/src/routes/(authenticated)/+page.server.ts @@ -7,10 +7,11 @@ export const load = async ({ fetch, cookies, url }) => { const baseFetchUrl = "http://localhost:3000/website"; if (searchQuery) { - params.append("website_title", `ilike.*${searchQuery}*`); + params.append("title", `ilike.*${searchQuery}*`); } switch (sortBy) { + case null: case "creation-time": params.append("order", "created_at.desc"); break; @@ -27,6 +28,19 @@ export const load = async ({ fetch, cookies, url }) => { const constructedFetchUrl = `${baseFetchUrl}?${params.toString()}`; + const totalWebsitesData = await fetch(baseFetchUrl, { + method: "HEAD", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}`, + Prefer: "count=exact" + } + }); + + const totalWebsiteCount = Number( + totalWebsitesData.headers.get("content-range")?.split("/").at(-1) + ); + const websiteData = await fetch(constructedFetchUrl, { method: "GET", headers: { @@ -38,6 +52,7 @@ export const load = async ({ fetch, cookies, url }) => { const websites = await websiteData.json(); return { + totalWebsiteCount, websites }; }; @@ -60,10 +75,10 @@ export const actions = { if (!res.ok) { const response = await res.json(); - return { createWebsite: { success: false, message: response.message } }; + return { success: false, message: response.message }; } - return { createWebsite: { success: true, operation: "created" } }; + return { success: true, message: "Successfully created website" }; }, updateWebsite: async ({ request, cookies, fetch }) => { const data = await request.formData(); @@ -81,10 +96,10 @@ export const actions = { if (!res.ok) { const response = await res.json(); - return { updateWebsite: { success: false, message: response.message } }; + return { success: false, message: response.message }; } - return { updateWebsite: { success: true, operation: "updated" } }; + return { success: true, message: "Successfully updated website" }; }, deleteWebsite: async ({ request, cookies, fetch }) => { const data = await request.formData(); @@ -99,9 +114,9 @@ export const actions = { if (!res.ok) { const response = await res.json(); - return { deleteWebsite: { success: false, message: response.message } }; + return { success: false, message: response.message }; } - return { deleteWebsite: { success: true, operation: "deleted" } }; + return { success: true, message: "Successfully deleted website" }; } }; diff --git a/web-app/src/routes/(authenticated)/+page.svelte b/web-app/src/routes/(authenticated)/+page.svelte index 2d00648..f8b8d16 100644 --- a/web-app/src/routes/(authenticated)/+page.svelte +++ b/web-app/src/routes/(authenticated)/+page.svelte @@ -1,31 +1,33 @@ +{#if form?.success} +

{form.message}

+{/if} + +{#if form?.success === false} +

{form.message}

+{/if} +

Create website

- {#if form?.createWebsite?.success} -

Successfully created website

- {/if} - - {#if form?.createWebsite?.success === false} -

{form.createWebsite.message}

- {/if} - @@ -33,71 +35,86 @@
-
-

Your websites

+{#if data.totalWebsiteCount > 0} +
+

All websites

- {#if form?.deleteWebsite?.success} -

Successfully deleted website

- {/if} +
+ + + + +
- {#if form?.deleteWebsite?.success === false} -

{form.deleteWebsite.message}

- {/if} + {#each data.websites as { id, content_type, title, created_at }} +
+

+ {title} +

+

+ Type: + {content_type} +

+

+ Created at: + +

+
+ Update +
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + + - {#each data.websites as { id, content_type, title, created_at }} -
-

- {title} -

-

- Type: - {content_type} -

-

- Created at: - -

-
- Update - { - return async ({ update }) => { - await update({ reset: false }); - }; - }} - > - {#if form?.updateWebsite?.success} -

Successfully updated website

- {/if} + + +
+
+ Delete +

+ Caution! + Deleting this website will irretrievably erase all data. +

+
+ - {#if form?.updateWebsite?.success === false} -

{form.updateWebsite.message}

- {/if} - - - - - -
-
-
- Delete - -
- - - -
-
-
- {/each} -
- -
-

Shared with you

-
+ + + + + {/each} +
+{/if} diff --git a/web-app/src/routes/(authenticated)/account/+page.server.ts b/web-app/src/routes/(authenticated)/account/+page.server.ts index 75b6153..2ac86a7 100644 --- a/web-app/src/routes/(authenticated)/account/+page.server.ts +++ b/web-app/src/routes/(authenticated)/account/+page.server.ts @@ -8,7 +8,7 @@ export const actions = { logout: async ({ cookies }) => { cookies.delete("session_token", { path: "/" }); - return { logout: { success: true } }; + return { success: true, message: "Successfully logged out" }; }, deleteAccount: async ({ request, fetch, cookies }) => { const data = await request.formData(); @@ -27,10 +27,10 @@ export const actions = { const response = await res.json(); if (!res.ok) { - return { deleteAccount: { success: false, message: response.message } }; + return { success: false, message: response.message }; } cookies.delete("session_token", { path: "/" }); - return { deleteAccount: { success: true } }; + return { success: true, message: "Successfully deleted account" }; } }; diff --git a/web-app/src/routes/(authenticated)/account/+page.svelte b/web-app/src/routes/(authenticated)/account/+page.svelte index 8efddd9..b10ee3e 100644 --- a/web-app/src/routes/(authenticated)/account/+page.svelte +++ b/web-app/src/routes/(authenticated)/account/+page.svelte @@ -4,6 +4,14 @@ const { data, form } = $props(); +{#if form?.success} +

{form.message}

+{/if} + +{#if form?.success === false} +

{form.message}

+{/if} +

Overview

@@ -20,10 +28,6 @@

Logout

- {#if form?.logout?.success} -

Successfully logged out

- {/if} -
@@ -32,17 +36,9 @@

Delete account

- {#if form?.deleteAccount?.success} -

Account was deleted

- {/if} - - {#if form?.deleteAccount?.success === false} -

{form.deleteAccount.message}

- {/if} -
diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts index 339100d..c051e53 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts @@ -1,5 +1,14 @@ export const load = async ({ params, fetch, cookies }) => { - const websiteData = await fetch(`http://localhost:3000/content?id=eq.${params.websiteId}`, { + const websiteData = await fetch(`http://localhost:3000/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 homeData = await fetch(`http://localhost:3000/home?website_id=eq.${params.websiteId}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -9,8 +18,10 @@ export const load = async ({ params, fetch, cookies }) => { }); const website = await websiteData.json(); + const home = await homeData.json(); return { - website + website, + home }; }; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts index 641eb71..e4f6b12 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts @@ -1,7 +1,4 @@ -import { randomUUID } from "node:crypto"; -import { mkdir, writeFile } from "node:fs/promises"; -import { extname, join, relative } from "node:path"; -import { ALLOWED_MIME_TYPES } from "$lib/utils.js"; +import { handleFileUpload } from "$lib/server/utils.js"; export const load = async ({ params, fetch, cookies, url }) => { const globalSettingsData = await fetch( @@ -28,15 +25,6 @@ export const load = async ({ params, fetch, cookies, url }) => { } ); - const homeData = await fetch(`http://localhost:3000/home?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 footerData = await fetch(`http://localhost:3000/footer?website_id=eq.${params.websiteId}`, { method: "GET", headers: { @@ -46,54 +34,14 @@ export const load = async ({ params, fetch, cookies, url }) => { } }); - const searchQuery = url.searchParams.get("article_search_query"); - const sortBy = url.searchParams.get("article_sort"); - - const parameters = new URLSearchParams(); - - const baseFetchUrl = `http://localhost:3000/article?website_id=eq.${params.websiteId}&select=*,media(*)`; - - if (searchQuery) { - parameters.append("title", `ilike.*${searchQuery}*`); - } - - switch (sortBy) { - case "creation-time": - parameters.append("order", "created_at.desc"); - break; - case "last-modified": - parameters.append("order", "last_modified_at.desc"); - break; - case "title-a-to-z": - parameters.append("order", "title.asc"); - break; - case "title-z-to-a": - parameters.append("order", "title.desc"); - break; - } - - const constructedFetchUrl = `${baseFetchUrl}&${parameters.toString()}`; - - const articlesData = await fetch(constructedFetchUrl, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${cookies.get("session_token")}` - } - }); - const globalSettings = await globalSettingsData.json(); const header = await headerData.json(); - const home = await homeData.json(); const footer = await footerData.json(); - const articles = await articlesData.json(); return { globalSettings, header, - home, - footer, - articles + footer }; }; @@ -134,8 +82,7 @@ export const actions = { return { success: true, - operation: "updated", - ressource: "global settings" + message: "Successfully updated global settings" }; }, updateHeader: async ({ request, fetch, cookies, locals, params }) => { @@ -174,8 +121,7 @@ export const actions = { return { success: true, - operation: "updated", - ressource: "header settings" + message: "Successfully updated header" }; }, updateHome: async ({ request, fetch, cookies, params }) => { @@ -197,7 +143,7 @@ export const actions = { return { success: false, message: response.message }; } - return { success: true, operation: "updated", ressource: "home settings" }; + return { success: true, message: "Successfully updated home" }; }, updateFooter: async ({ request, fetch, cookies, params }) => { const data = await request.formData(); @@ -220,149 +166,7 @@ export const actions = { return { success: true, - operation: "updated", - ressource: "footer settings" + message: "Successfully updated footer" }; - }, - createArticle: async ({ request, fetch, cookies, params }) => { - const data = await request.formData(); - - const res = await fetch("http://localhost:3000/article", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${cookies.get("session_token")}` - }, - body: JSON.stringify({ - website_id: params.websiteId, - title: data.get("title") - }) - }); - - if (!res.ok) { - const response = await res.json(); - return { success: false, message: response.message }; - } - - return { success: true, operation: "created", ressource: "article" }; - }, - editArticle: async ({ request, fetch, cookies, locals, params }) => { - const data = await request.formData(); - - const coverFile = data.get("cover-image") as File; - const cover = await handleFileUpload( - coverFile, - params.websiteId, - locals.user.id, - cookies.get("session_token"), - fetch - ); - - if (cover?.success === false) { - return cover; - } - - const res = await fetch(`http://localhost:3000/article?id=eq.${data.get("article-id")}`, { - method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${cookies.get("session_token")}` - }, - body: JSON.stringify({ - title: data.get("title"), - meta_description: data.get("description"), - meta_author: data.get("author"), - cover_image: cover?.content, - publication_date: data.get("publication-date"), - main_content: data.get("main-content") - }) - }); - - if (!res.ok) { - const response = await res.json(); - return { success: false, message: response.message }; - } - - return { success: true, operation: "updated", ressource: "article" }; - }, - deleteArticle: async ({ request, fetch, cookies }) => { - const data = await request.formData(); - - const res = await fetch(`http://localhost:3000/article?id=eq.${data.get("article-id")}`, { - 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 }; - } - - return { success: true, operation: "deleted", ressource: "article" }; } }; - -const handleFileUpload = async ( - file: File, - contentId: string, - userId: string, - session_token: string | undefined, - customFetch: typeof fetch -) => { - if (file.size === 0) return undefined; - - const MAX_FILE_SIZE = 1024 * 1024; - - if (file.size > MAX_FILE_SIZE) { - return { - success: false, - message: `File size exceeds the maximum limit of ${MAX_FILE_SIZE / 1024 / 1024} MB.` - }; - } - - if (!ALLOWED_MIME_TYPES.includes(file.type)) { - return { - success: false, - message: "Invalid file type. JPEG, PNG, SVG and WEBP are allowed." - }; - } - - const buffer = Buffer.from(await file.arrayBuffer()); - const uploadDir = join(process.cwd(), "static", "user-uploads", userId); - await mkdir(uploadDir, { recursive: true }); - - const fileId = randomUUID(); - const fileExtension = extname(file.name); - const filepath = join(uploadDir, `${fileId}${fileExtension}`); - - await writeFile(filepath, buffer); - - const relativePath = relative(join(process.cwd(), "static"), filepath); - - const res = await customFetch("http://localhost:3000/media", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${session_token}`, - Prefer: "return=representation", - Accept: "application/vnd.pgrst.object+json" - }, - body: JSON.stringify({ - website_id: contentId, - user_id: userId, - original_name: file.name, - file_system_path: relativePath - }) - }); - - const response = await res.json(); - - if (!res.ok) { - return { success: false, message: response.message }; - } - - return { success: true, content: response.id }; -}; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte index 237eb6f..302db35 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.svelte @@ -1,15 +1,133 @@ -
-

Settings

-
+ -
-

Collaborators

-
+{#if form?.success} +

{form.message}

+{/if} -
-

Logs

-
+{#if form?.success === false} +

{form.message}

+{/if} + + +
+

Settings

+ +
+

Global

+ { + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + + + + + + +
+ +
+

Header

+ +
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + + + + + +
+
+ +
+

Home

+ +
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + +
+
+ +
+

Footer

+ +
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + +
+
+
+
diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts new file mode 100644 index 0000000..8c917ff --- /dev/null +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts @@ -0,0 +1,103 @@ +export const load = async ({ params, fetch, cookies, url, parent }) => { + const searchQuery = url.searchParams.get("article_search_query"); + const sortBy = url.searchParams.get("article_sort"); + + const parameters = new URLSearchParams(); + + const baseFetchUrl = `http://localhost:3000/article?website_id=eq.${params.websiteId}&select=id,title`; + + if (searchQuery) { + parameters.append("title", `ilike.*${searchQuery}*`); + } + + switch (sortBy) { + case null: + case "creation-time": + parameters.append("order", "created_at.desc"); + break; + case "last-modified": + parameters.append("order", "last_modified_at.desc"); + break; + case "title-a-to-z": + parameters.append("order", "title.asc"); + break; + case "title-z-to-a": + parameters.append("order", "title.desc"); + break; + } + + const constructedFetchUrl = `${baseFetchUrl}&${parameters.toString()}`; + + const totalArticlesData = await fetch(baseFetchUrl, { + method: "HEAD", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}`, + Prefer: "count=exact" + } + }); + + const totalArticleCount = Number( + totalArticlesData.headers.get("content-range")?.split("/").at(-1) + ); + + const articlesData = await fetch(constructedFetchUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + } + }); + + const articles = await articlesData.json(); + const { website } = await parent(); + + return { + totalArticleCount, + articles, + website + }; +}; + +export const actions = { + createArticle: async ({ request, fetch, cookies, params }) => { + const data = await request.formData(); + + const res = await fetch("http://localhost:3000/article", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + }, + body: JSON.stringify({ + website_id: params.websiteId, + title: data.get("title") + }) + }); + + if (!res.ok) { + const response = await res.json(); + return { success: false, message: response.message }; + } + + return { success: true, message: "Successfully created article" }; + }, + deleteArticle: async ({ request, fetch, cookies }) => { + const data = await request.formData(); + + const res = await fetch(`http://localhost:3000/article?id=eq.${data.get("id")}`, { + 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 }; + } + + return { success: true, message: "Successfully deleted article" }; + } +}; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte new file mode 100644 index 0000000..2d1a6f8 --- /dev/null +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.svelte @@ -0,0 +1,82 @@ + + +{#if form?.success} +

{form.message}

+{/if} + +{#if form?.success === false} +

{form.message}

+{/if} + + +
+

Create article

+ +
+ + + +
+
+ + {#if data.totalArticleCount > 0} +
+

All articles

+ +
+ + + +
+ + {#each data.articles as { id, title }} +
+

{title}

+ Edit +
+ Delete +

+ Caution! + Deleting this article will irretrievably erase all data. +

+
+ + + +
+
+
+ {/each} +
+ {/if} +
diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts new file mode 100644 index 0000000..6113ec6 --- /dev/null +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.server.ts @@ -0,0 +1,59 @@ +import { handleFileUpload } from "$lib/server/utils.js"; + +export const load = async ({ parent, params, cookies, fetch }) => { + const articleData = await fetch(`http://localhost:3000/article?id=eq.${params.articleId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}`, + Accept: "application/vnd.pgrst.object+json" + } + }); + + const article = await articleData.json(); + const { website } = await parent(); + + return { website, article }; +}; + +export const actions = { + default: async ({ fetch, cookies, request, params, locals }) => { + const data = await request.formData(); + + const coverFile = data.get("cover-image") as File; + const cover = await handleFileUpload( + coverFile, + params.websiteId, + locals.user.id, + cookies.get("session_token"), + fetch + ); + + if (cover?.success === false) { + return cover; + } + + const res = await fetch(`http://localhost:3000/article?id=eq.${params.articleId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + }, + body: JSON.stringify({ + title: data.get("title"), + meta_description: data.get("description"), + meta_author: data.get("author"), + cover_image: cover?.content, + publication_date: data.get("publication-date"), + 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 updated article" }; + } +}; diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte new file mode 100644 index 0000000..b4f4790 --- /dev/null +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/[articleId]/+page.svelte @@ -0,0 +1,62 @@ + + +{#if form?.success} +

{form.message}

+{/if} + +{#if form?.success === false} +

{form.message}

+{/if} + + +
+

Edit article

+ +
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + + + + + + + + +
+
+
diff --git a/web-app/src/routes/+layout.svelte b/web-app/src/routes/+layout.svelte index de0cab5..74896a1 100644 --- a/web-app/src/routes/+layout.svelte +++ b/web-app/src/routes/+layout.svelte @@ -1,11 +1,14 @@ -
-

{$page.url.pathname}

-
+{#if !isProjectRoute} +
+

{$page.url.pathname}

+
+{/if} -
+
{@render children()}

- archtika + archtika is a free, open, modern, performant and lightweight CMS

+ + diff --git a/web-app/svelte.config.ts b/web-app/svelte.config.ts index 4dca81a..8123f69 100644 --- a/web-app/svelte.config.ts +++ b/web-app/svelte.config.ts @@ -1,4 +1,4 @@ -import adapter from "@sveltejs/adapter-auto"; +import adapter from "@sveltejs/adapter-node"; import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; /** @type {import('@sveltejs/kit').Config} */