Skip to content

Commit

Permalink
Merge pull request #186 from rxhul18/feat-feedback-full
Browse files Browse the repository at this point in the history
feat: feedback-modal
  • Loading branch information
SkidGod4444 authored Jan 12, 2025
2 parents 4ecd23b + 3de0c4e commit 198b13b
Show file tree
Hide file tree
Showing 14 changed files with 427 additions and 66 deletions.
111 changes: 111 additions & 0 deletions apps/api/app/v1/[[...route]]/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { zValidator } from "@hono/zod-validator";
import { feedbackSchema } from "@repo/types";
import { Hono } from "hono";
import { auth } from "@plura/auth";
import { prisma } from "@plura/db";
import { checkLogin } from "@/app/actions/checkLogin";
import { checkAdmin } from "@/app/actions/checkAdmin";

const app = new Hono()
.post("/", zValidator("json", feedbackSchema), async (c) => {
const session = await auth.api.getSession({
headers: c.req.raw.headers,
});
if (!session?.user.id) {
return c.json({ message: "Unauthorized", status: 401 }, 401);
}
const body = c.req.valid("json");
try {
await prisma.feedback.create({
data: {
userId: session.user.id,
desc: body.desc,
emotion: body.emotion,
},
});
return c.json(
{
message: "Successfully submitted!",
},
200,
);
} catch (error) {
console.error("Error creating feedback:", error);
return c.json(
{
message: "Error submitting feedback",
status: 500,
},
500,
);
}
})
.use(checkLogin, checkAdmin)
.get("/all", async (c) => {
try {
const feedbacks = await prisma.feedback.findMany();
return c.json(
{
message: "All feedback retrieved successfully",
data: feedbacks,
},
200,
);
} catch (error) {
console.error("Error fetching all feedback:", error);
return c.json(
{
message: "Error fetching feedback",
status: 500,
},
500,
);
}
})
.get("/:id", async (c) => {
const userId = c.req.param("id");
if (!userId) {
return c.json(
{
message: "User ID is required",
status: 400,
},
400,
);
}
try {
const feedbacks = await prisma.feedback.findMany({
where: {
userId: userId,
},
});
if (feedbacks.length === 0) {
return c.json(
{
message: "No feedback found for this user",
status: 404,
},
404,
);
}
return c.json(
{
message: "Feedback retrieved successfully",
status: 200,
data: feedbacks,
},
200,
);
} catch (error) {
console.error("Error fetching feedback:", error);
return c.json(
{
message: "Error fetching feedback",
status: 500,
},
500,
);
}
});

export default app;
4 changes: 3 additions & 1 deletion apps/api/app/v1/[[...route]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import health from "./health";
import user from "./user";
import project from "./project";
import contributors from "./contributors";
import feedback from "./feedback";
import { cors } from "hono/cors";
import workspace from "./workspace";
import { Ratelimit } from "@upstash/ratelimit";
Expand Down Expand Up @@ -75,7 +76,8 @@ app.route("/auth", auth);
app.route("/user", user);
app.route("/contributors", contributors);
app.route("/workspace", workspace);
app.route("project", project);
app.route("/project", project);
app.route("/feedback", feedback);
app.route("/workflow", workflow);

const GET = handle(app);
Expand Down
36 changes: 36 additions & 0 deletions apps/app/actions/feedback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use server";

import { betterFetch } from "@better-fetch/fetch";
import { getSession } from "./session";
import { headers } from "next/headers";


const API_ENDPOINT =
process.env.NODE_ENV === "production"
? "https://api.plura.pro/"
: "http://localhost:3001";

export const createFeedback = async ({
desc,
emotion
}: {
desc: string;
emotion: string;
}) => {
const user = await getSession();
if (!user) {
return;
}
const feedback = await betterFetch(`${API_ENDPOINT}/v1/feedback`, {
method: "POST",
body: {
desc:desc,
emotion:emotion
},
headers: {
cookie: (await headers()).get("cookie") || "",
},
});

return feedback;
};
129 changes: 129 additions & 0 deletions apps/app/components/custom/feedback-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* eslint-disable react/no-unescaped-entities */
'use client';
import { Button } from "@/components/ui/button"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { toast } from "sonner";
import { Profanity } from "profanity-validator";
import { Frown, Meh, MessageSquare, Smile } from 'lucide-react';
import { Textarea } from "../ui/textarea";
import { SubmitHandler, useForm } from "react-hook-form";
import { z } from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/components/ui/form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useState } from "react";
import { createFeedback } from "@/actions/feedback";

const profanity = new Profanity({
customWords: [""],
heat: 0.9,
});

const profanityCheck = async (value: string) => {
const result = await profanity.validateField(value);
return result.isValid;
};

const postSchema = z.object({
description: z
.string()
.min(10, "Description must be at least 10 characters")
.refine(async (val) => await profanityCheck(val), {
message: "Inappropriate content detected in description",
})
});
type PostSchema = z.infer<typeof postSchema>;

export function FeedbackModal() {
const [selectedEmo, setSelectedEmo] = useState<'happy' | 'idle' | 'sad'>("happy");

const form = useForm<PostSchema>({
resolver: zodResolver(postSchema),
});

const onSubmit: SubmitHandler<PostSchema> = async (data) => {
try {
const validatedData = await postSchema.parseAsync({ ...data });
const res = await createFeedback({
desc: validatedData.description,
emotion: selectedEmo
});
return res;
} catch (error) {
toast.error(`Error in submiting feeback !Please try again `);
} finally {
toast.success(`Thanks for your feedback!`);
}
};

return (
<Dialog>
<DialogTrigger asChild>
<Button variant="default"> <MessageSquare /> Feedback </Button>
</DialogTrigger>
<DialogContent className="max-w-[425px] md:min-w-[480px] border">
<DialogHeader className="!text-center w-full pt-5 pb-2">
<DialogTitle className="text-2xl text-primary">Leave feedback</DialogTitle>
<DialogDescription>
We'd love to hear what went well or how we can improve the product experience.
</DialogDescription>
</DialogHeader>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 w-full max-w-md mx-auto"
>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormControl>
<Textarea {...field} placeholder="Can you ..." className="border min-h-[100px]" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid gap-4 py-4">
<div className="flex gap-2">
<Button
size="icon" variant="secondary" className={`hover:bg-secondary transition-all duration-200 ${selectedEmo === 'happy' ? 'border-primary border-[1px]' : ''}`}
onClick={() => setSelectedEmo('happy')} type="button"
>
<Smile />
</Button>
<Button size="icon" variant="secondary" className={`hover:bg-secondary transition-all duration-200 ${selectedEmo === 'idle' ? 'border-primary border-[1px]' : ''}`}
onClick={() => setSelectedEmo('idle')} type="button"
>
<Meh />
</Button>
<Button
size="icon" variant="secondary" className={`hover:bg-secondary transition-all duration-200 ${selectedEmo === 'sad' ? 'border-primary border-[1px]' : ''}`}
onClick={() => setSelectedEmo('sad')} type="button"
>
<Frown />
</Button>
</div>
</div>
<Button type="submit" className="w-full">
{form.formState.isSubmitting ? "Checking..." : "Submit"}
</Button>
</form>
</Form>
</DialogContent>
</Dialog>
)
}
Loading

0 comments on commit 198b13b

Please sign in to comment.