Skip to content

Commit 459e104

Browse files
committed
feat(feedback): implement feedback form and notification system with discord integration
1 parent 3e996c2 commit 459e104

File tree

7 files changed

+169
-9
lines changed

7 files changed

+169
-9
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
NEXT_PUBLIC_SUPABASE_URL=<url>
22
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon_key>
3+
DISCORD_WEBHOOK_URL=<url>

src/app/(public)/feedback/page.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,21 @@ import type { Metadata } from "next";
22

33
import { Heading } from "@/components/ui";
44
import { PROJECT_NAME } from "@/config/constants";
5+
import { FeedbackForm } from "@/components/feedback";
56

67
export const metadata: Metadata = {
78
title: `Feedback - ${PROJECT_NAME}`,
8-
description: "Share your feedback with us",
9+
description: "Have a question? We will be happy to help you.",
910
};
1011

1112
export default function FeedbackPage() {
1213
return (
13-
<main className="max-w-screen-xl mx-auto p-4">
14-
<Heading title="Feedback" subtitle="Share your feedback with us" />
15-
<p>
16-
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quae tenetur
17-
itaque tempora. Nostrum ut architecto libero! Quasi, iure placeat
18-
doloribus tenetur ullam veniam esse vero nemo! Molestias inventore velit
19-
praesentium!
20-
</p>
14+
<main className="max-w-lg mx-auto p-4">
15+
<Heading
16+
title="Feedback"
17+
subtitle="Have a question? We will be happy to help you."
18+
/>
19+
<FeedbackForm />
2120
</main>
2221
);
2322
}

src/app/api/messages/route.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NextResponse } from "next/server";
2+
import { messageNotifier } from "@/lib/observer";
3+
import { DiscordObserver } from "@/lib/dicord";
4+
5+
export async function POST(req: Request) {
6+
try {
7+
messageNotifier.subscribe(DiscordObserver);
8+
9+
const data = await req.json();
10+
await messageNotifier.notify(data);
11+
12+
return NextResponse.json({ message: "Notification sent" });
13+
} catch (error) {
14+
console.error(error);
15+
return NextResponse.json(
16+
{ error: "Error processing the request" },
17+
{ status: 500 },
18+
);
19+
}
20+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"use client";
2+
3+
import type { FeedbackForm as FeedbackFormType } from "@/types";
4+
5+
import { useState } from "react";
6+
import { useRouter } from "next/navigation";
7+
import { Input, Textarea, Button, addToast } from "@heroui/react";
8+
import { useForm } from "react-hook-form";
9+
import { zodResolver } from "@hookform/resolvers/zod";
10+
import { feedbackSchema } from "@/lib/zod";
11+
import { HOME_PATH } from "@/config/constants";
12+
13+
export default function FeedbackForm() {
14+
const router = useRouter();
15+
const [apiError, setApiError] = useState<string | null>(null);
16+
17+
const {
18+
register,
19+
handleSubmit,
20+
formState: { errors, isSubmitting },
21+
reset,
22+
} = useForm<FeedbackFormType>({
23+
resolver: zodResolver(feedbackSchema),
24+
});
25+
26+
const onSubmit = async (data: FeedbackFormType) => {
27+
try {
28+
const response = await fetch("/api/messages", {
29+
method: "POST",
30+
headers: { "Content-Type": "application/json" },
31+
body: JSON.stringify(data),
32+
});
33+
34+
if (!response.ok) throw new Error("Failed to send message");
35+
36+
addToast({
37+
title: "Thank you for your feedback!",
38+
description:
39+
"We appreciate your feedback and will use it to continuously improve. Your opinion is valuable to us.",
40+
});
41+
42+
reset();
43+
router.push(HOME_PATH);
44+
} catch (error) {
45+
console.error(error);
46+
setApiError("There was a problem submitting the form.");
47+
}
48+
};
49+
50+
return (
51+
<form onSubmit={handleSubmit(onSubmit)} className="py-4 space-y-8">
52+
<Input
53+
size="sm"
54+
type="text"
55+
label="Name"
56+
labelPlacement="outside"
57+
placeholder="Joe Doe"
58+
isInvalid={!!errors.name?.message}
59+
color={errors.name?.message ? "danger" : "default"}
60+
errorMessage={errors.name?.message}
61+
{...register("name")}
62+
/>
63+
<Input
64+
size="sm"
65+
type="email"
66+
label="Email"
67+
labelPlacement="outside"
68+
placeholder="you@example.com"
69+
isInvalid={!!errors.email?.message}
70+
color={errors.email?.message ? "danger" : "default"}
71+
errorMessage={errors.email?.message}
72+
{...register("email")}
73+
/>
74+
<Textarea
75+
size="sm"
76+
type="textarea"
77+
label="Message"
78+
labelPlacement="outside"
79+
placeholder="Your message here..."
80+
classNames={{ base: "!mt-2" }}
81+
isInvalid={!!errors.message?.message}
82+
color={errors.message?.message ? "danger" : "default"}
83+
errorMessage={errors.message?.message}
84+
{...register("message")}
85+
/>
86+
{apiError && <p className="text-red-500">{apiError}</p>}
87+
<div className="flex justify-start gap-2">
88+
<Button
89+
size="sm"
90+
type="submit"
91+
disabled={isSubmitting}
92+
className="bg-neutral-950 dark:bg-white text-white dark:text-neutral-950 font-medium"
93+
>
94+
{isSubmitting ? "Sending..." : "Send"}
95+
</Button>
96+
</div>
97+
</form>
98+
);
99+
}

src/components/feedback/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as FeedbackForm } from "./feedback-form";

src/lib/dicord.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Observer } from "./observer";
2+
3+
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL!;
4+
5+
export const DiscordObserver: Observer = {
6+
async update(data) {
7+
const name = `# ${data["name"]}\n\n`;
8+
const email = `- Email: ${data["email"]}\n`;
9+
const message = `- Message: ${data["message"]}`;
10+
11+
await fetch(DISCORD_WEBHOOK_URL, {
12+
method: "POST",
13+
headers: { "Content-Type": "application/json" },
14+
body: JSON.stringify({ content: `${name}${email}${message}` }),
15+
});
16+
},
17+
};

src/lib/observer.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { FeedbackForm } from "@/types";
2+
3+
export type Observer = {
4+
update: (data: FeedbackForm) => Promise<void>;
5+
};
6+
7+
class Subject {
8+
private observers: Observer[] = [];
9+
10+
subscribe(observer: Observer) {
11+
this.observers.push(observer);
12+
}
13+
14+
unsubscribe(observer: Observer) {
15+
this.observers = this.observers.filter((obs) => obs !== observer);
16+
}
17+
18+
async notify(data: FeedbackForm) {
19+
await Promise.all(this.observers.map((observer) => observer.update(data)));
20+
}
21+
}
22+
23+
export const messageNotifier = new Subject();

0 commit comments

Comments
 (0)