From 55798fd29495aa80148172dc4f0f2b95ef528bcc Mon Sep 17 00:00:00 2001
From: bytedream <bytedream@protonmail.com>
Date: Sat, 30 Nov 2024 03:00:46 +0100
Subject: [PATCH] add public feedback/contact option

---
 src/lib/server/database.ts                    |   6 +-
 src/routes/+layout.svelte                     |   6 +
 src/routes/feedback/+layout.svelte            |   5 +
 src/routes/feedback/+page.svelte              | 122 ++++++++++++++++++
 src/routes/feedback/+page.ts                  |   7 -
 src/routes/feedback/+server.ts                |  20 +++
 .../feedback/[...url_hash]/+layout.svelte     |   3 -
 .../feedback/[...url_hash]/+page.svelte       |  28 ++--
 src/routes/feedback/schema.ts                 |   6 +
 static/img/menu-feedback.png                  | Bin 0 -> 152 bytes
 10 files changed, 175 insertions(+), 28 deletions(-)
 create mode 100644 src/routes/feedback/+layout.svelte
 create mode 100644 src/routes/feedback/+page.svelte
 delete mode 100644 src/routes/feedback/+page.ts
 create mode 100644 src/routes/feedback/+server.ts
 delete mode 100644 src/routes/feedback/[...url_hash]/+layout.svelte
 create mode 100644 src/routes/feedback/schema.ts
 create mode 100644 static/img/menu-feedback.png

diff --git a/src/lib/server/database.ts b/src/lib/server/database.ts
index d95f736..ac090f3 100644
--- a/src/lib/server/database.ts
+++ b/src/lib/server/database.ts
@@ -99,13 +99,13 @@ export class StrikePunishment extends Model {
 
 @Table({ modelName: 'feedback', underscored: true })
 export class Feedback extends Model {
-	@Column({ type: DataTypes.STRING, allowNull: false, unique: true })
-	@Index
-	declare url_hash: string;
 	@Column({ type: DataTypes.STRING, allowNull: false })
 	declare event: string;
 	@Column({ type: DataTypes.STRING })
 	declare content: string;
+	@Column({ type: DataTypes.STRING, allowNull: false, unique: true })
+	@Index
+	declare url_hash: string;
 	@Column({ type: DataTypes.INTEGER })
 	@ForeignKey(() => User)
 	declare user_id: number;
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 6194bc2..09f6f43 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -29,6 +29,12 @@
 			href: `${env.PUBLIC_BASE_PATH}/faq`,
 			active: false
 		},
+		{
+			name: 'Feedback & Kontakt',
+			sprite: `${env.PUBLIC_BASE_PATH}/img/menu-feedback.png`,
+			href: `${env.PUBLIC_BASE_PATH}/feedback`,
+			active: false
+		},
 		{
 			name: 'Team',
 			sprite: `${env.PUBLIC_BASE_PATH}/img/menu-team.png`,
diff --git a/src/routes/feedback/+layout.svelte b/src/routes/feedback/+layout.svelte
new file mode 100644
index 0000000..7d25c0b
--- /dev/null
+++ b/src/routes/feedback/+layout.svelte
@@ -0,0 +1,5 @@
+<div class="flex justify-center items-center w-full min-h-screen h-full">
+	<div class="absolute top-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg">
+		<slot />
+	</div>
+</div>
diff --git a/src/routes/feedback/+page.svelte b/src/routes/feedback/+page.svelte
new file mode 100644
index 0000000..1287757
--- /dev/null
+++ b/src/routes/feedback/+page.svelte
@@ -0,0 +1,122 @@
+<script lang="ts">
+	import Input from '$lib/components/Input/Input.svelte';
+	import Textarea from '$lib/components/Input/Textarea.svelte';
+	import { env } from '$env/dynamic/public';
+	import Select from '$lib/components/Input/Select.svelte';
+
+	let content = '';
+	let type = 'feedback';
+
+	let submitModal: HTMLDialogElement;
+	let sentModal: HTMLDialogElement & { type?: string } = {};
+
+	async function submitFeedback() {
+		await fetch(`${env.PUBLIC_BASE_PATH}/feedback`, {
+			method: 'POST',
+			body: JSON.stringify({
+				content: content,
+				type: type
+			})
+		});
+	}
+</script>
+
+<svelte:head>
+	<title>Feedback & Kontakt</title>
+</svelte:head>
+
+<div>
+	<h2 class="text-3xl text-center">Feedback & Kontakt</h2>
+	<form on:submit|preventDefault={() => submitModal.show()}>
+		<div class="space-y-4 mt-6 mb-4">
+			<Select size="sm" bind:value={type}>
+				<option value="feedback">Feedback</option>
+				<option value="contact">Kontakt</option>
+			</Select>
+			<Textarea
+				required={true}
+				rows={4}
+				label={type === 'feedback' ? 'Feedback' : 'Anfrage'}
+				bind:value={content}
+			/>
+			<div>
+				<Input type="submit" disabled={type === '' || content === ''} value="Senden" />
+			</div>
+		</div>
+	</form>
+</div>
+
+<dialog class="modal" bind:this={submitModal}>
+	<form method="dialog" class="modal-box">
+		<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
+		<div>
+			<h3 class="font-roboto font-medium text-xl">
+				{type === 'feedback' ? 'Feedback' : 'Kontaktanfrage'} abschicken?
+			</h3>
+			<div class="my-4">
+				{#if type === 'feedback'}
+					<p>Nach dem Abschicken des Feedbacks lässt es sich nicht mehr bearbeiten.</p>
+				{:else}
+					<p>
+						Bitte hinterlege eine Rückmeldemöglichkeit in deiner Anfrage. Nachdem sie abgeschickt
+						wurde, kannst du die Nachricht nicht mehr bearbeiten.
+					</p>
+				{/if}
+			</div>
+			<div class="flex flex-row space-x-1">
+				<Input
+					type="submit"
+					value="Abschicken"
+					on:click={async () => {
+						await submitFeedback();
+						sentModal.type = type;
+						sentModal.show();
+					}}
+				/>
+				<Input type="submit" value="Abbrechen" />
+			</div>
+		</div>
+	</form>
+	<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.3)]">
+		<button>close</button>
+	</form>
+</dialog>
+
+<dialog class="modal" bind:this={sentModal}>
+	<form
+		method="dialog"
+		class="modal-box"
+		on:submit={() => {
+			content = '';
+			type = 'feedback';
+		}}
+	>
+		<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
+		<div>
+			<h3 class="font-roboto font-medium text-xl">
+				{sentModal.type === 'feedback' ? 'Feedback' : 'Kontaktanfrage'} abgeschickt
+			</h3>
+			<div class="my-4">
+				{#if type === 'feedback'}
+					<p>Dein Feedback wurde abgeschickt.</p>
+				{:else}
+					<p>
+						Deine Kontaktanfrage wurde abgeschickt. Jemand aus dem Team wird sich nächstmöglich bei
+						Dir melden.
+					</p>
+				{/if}
+			</div>
+			<Input type="submit" value="Schließen" />
+		</div>
+	</form>
+	<form
+		method="dialog"
+		class="modal-backdrop bg-[rgba(0,0,0,.3)]"
+		on:submit={() => {
+			content = '';
+			type = 'feedback';
+		}}
+	>
+		<button>close</button>
+	</form>
+</dialog>
diff --git a/src/routes/feedback/+page.ts b/src/routes/feedback/+page.ts
deleted file mode 100644
index 826ac63..0000000
--- a/src/routes/feedback/+page.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { PageLoad } from './$types';
-import { redirect } from '@sveltejs/kit';
-import { env } from '$env/dynamic/public';
-
-export const load: PageLoad = async () => {
-	throw redirect(302, `${env.PUBLIC_BASE_PATH}/`);
-};
diff --git a/src/routes/feedback/+server.ts b/src/routes/feedback/+server.ts
new file mode 100644
index 0000000..d778e46
--- /dev/null
+++ b/src/routes/feedback/+server.ts
@@ -0,0 +1,20 @@
+import type { RequestHandler } from '@sveltejs/kit';
+import { Feedback } from '$lib/server/database';
+import { FeedbackSubmitSchema } from './schema';
+import crypto from 'crypto';
+
+export const POST = (async ({ request }) => {
+	const parseResult = await FeedbackSubmitSchema.safeParseAsync(await request.json());
+	if (!parseResult.success) {
+		return new Response(null, { status: 400 });
+	}
+	const data = parseResult.data;
+
+	await Feedback.create({
+		event: `website-${data.type}`,
+		content: data.content,
+		url_hash: crypto.randomBytes(18).toString('hex')
+	});
+
+	return new Response(null, { status: 200 });
+}) satisfies RequestHandler;
diff --git a/src/routes/feedback/[...url_hash]/+layout.svelte b/src/routes/feedback/[...url_hash]/+layout.svelte
deleted file mode 100644
index 9001a18..0000000
--- a/src/routes/feedback/[...url_hash]/+layout.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-<div class="flex justify-center items-center w-full min-h-screen h-full">
-	<slot />
-</div>
diff --git a/src/routes/feedback/[...url_hash]/+page.svelte b/src/routes/feedback/[...url_hash]/+page.svelte
index 022c0fc..56cee00 100644
--- a/src/routes/feedback/[...url_hash]/+page.svelte
+++ b/src/routes/feedback/[...url_hash]/+page.svelte
@@ -13,18 +13,16 @@
 	<meta name="robots" content="noindex" />
 </svelte:head>
 
-<div class="absolute top-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg">
-	{#if data.draft}
-		<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
-			<FeedbackDraft
-				event={data.event}
-				anonymous={data.anonymous}
-				on:submit={() => (data.draft = false)}
-			/>
-		</div>
-	{:else}
-		<div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}>
-			<FeedbackSent />
-		</div>
-	{/if}
-</div>
+{#if data.draft}
+	<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
+		<FeedbackDraft
+			event={data.event}
+			anonymous={data.anonymous}
+			on:submit={() => (data.draft = false)}
+		/>
+	</div>
+{:else}
+	<div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}>
+		<FeedbackSent />
+	</div>
+{/if}
diff --git a/src/routes/feedback/schema.ts b/src/routes/feedback/schema.ts
new file mode 100644
index 0000000..c6fb53a
--- /dev/null
+++ b/src/routes/feedback/schema.ts
@@ -0,0 +1,6 @@
+import { z } from 'zod';
+
+export const FeedbackSubmitSchema = z.object({
+	content: z.string(),
+	type: z.enum(['feedback', 'contact'])
+});
diff --git a/static/img/menu-feedback.png b/static/img/menu-feedback.png
new file mode 100644
index 0000000000000000000000000000000000000000..074373ff1b640c9c2dde2296a114fc213c60fa41
GIT binary patch
literal 152
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`@t!V@Ar`$?CvN0rFyvs~{GXxc
zun^Cb>D`kx3|HP0l1-N`Joi2%^!v?(TqOggbz7QLdESUENngf3Sto!kd4}sNrv6vo
zU8U8ws`tNF+j#$usS)FrZ04`0R@Yy4N^X{}Js*9qtim(I5NH*Hr>mdKI;Vst0Oyl6
Avj6}9

literal 0
HcmV?d00001