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