From 170f9b9972c2c82438a54604a82bfda22c5a6286 Mon Sep 17 00:00:00 2001 From: filipapeixoto25 Date: Thu, 27 Nov 2025 19:11:35 +0000 Subject: [PATCH] feat: report an issue form & backoffice page --- src/app/(app)/layout.tsx | 2 + .../settings/backoffice/feedbacks/page.tsx | 181 ++++++++++++++++++ .../feedback/add-feedback-content.tsx | 83 ++++++++ src/components/feedback/banner.tsx | 74 +++++++ src/components/feedback/modal.tsx | 72 +++++++ src/components/sidebar-settings.tsx | 6 + src/lib/feedback.ts | 17 ++ src/lib/mutations/feedback.ts | 22 +++ src/lib/queries/feedback.ts | 10 + 9 files changed, 467 insertions(+) create mode 100644 src/app/(app)/settings/backoffice/feedbacks/page.tsx create mode 100644 src/components/feedback/add-feedback-content.tsx create mode 100644 src/components/feedback/banner.tsx create mode 100644 src/components/feedback/modal.tsx create mode 100644 src/lib/feedback.ts create mode 100644 src/lib/mutations/feedback.ts create mode 100644 src/lib/queries/feedback.ts diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx index 0c7ae9c..5768e6b 100644 --- a/src/app/(app)/layout.tsx +++ b/src/app/(app)/layout.tsx @@ -2,6 +2,7 @@ import { Metadata } from "next"; import { AuthCheck } from "@/components/auth-check"; import Navbar from "@/components/navbar"; import { InstallPromptProvider } from "@/contexts/install-prompt-provider"; +import FeedbackBanner from "@/components/feedback/banner"; export const metadata: Metadata = { title: "Calendar | Pombo", @@ -48,6 +49,7 @@ export default function AppLayout({
+
{children} diff --git a/src/app/(app)/settings/backoffice/feedbacks/page.tsx b/src/app/(app)/settings/backoffice/feedbacks/page.tsx new file mode 100644 index 0000000..be271a7 --- /dev/null +++ b/src/app/(app)/settings/backoffice/feedbacks/page.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { useGetFeedbacks } from "@/lib/queries/feedback"; +import { useDeleteFeedback } from "@/lib/mutations/feedback"; +import { useState, Fragment } from "react"; +import { + Transition, + TransitionChild, + Dialog, + DialogPanel, +} from "@headlessui/react"; + +function ConfirmDelete({ + state, + setState, + onConfirm, + errorMessage, +}: { + state: boolean; + setState: (state: boolean) => void; + onConfirm: () => void; + errorMessage: string | null; +}) { + return ( + + setState(false)} + > + +
+ + +
+ + +

+ Are you sure you want to delete this feedback report? +

+ {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ + +
+
+
+
+
+
+ ); +} + +interface IFeedback { + id: string; + subject: string; + message: string; + user_id: string; + inserted_at: string; + updated_at: string; +} + +export default function Feedbacks() { + const [errorMessage, setErrorMessage] = useState(null); + + const [isConfirmationVisible, setConfirmationVisible] = useState(false); + const [selectedFeedbackId, setSelectedFeedbackId] = useState( + null, + ); + + const { data: feedbacksRaw } = useGetFeedbacks(); + const feedbacks: IFeedback[] = feedbacksRaw?.data ?? []; + + const deleteMutation = useDeleteFeedback(); + + const handleDelete = (id: string) => { + setErrorMessage(null); + + deleteMutation.mutate(id, { + onSuccess: () => { + setConfirmationVisible(false); + setSelectedFeedbackId(null); + }, + onError: () => setErrorMessage("An error occurred."), + }); + }; + + return ( +
+
+

Feedbacks

+

View submitted feedback reports

+
+
+
+ + + + + + + + + + + + {feedbacks.map((fb: IFeedback) => ( + + + + + + + ))} + +
+ Subject + + Message + + Submitted + + Actions +
+ {fb.subject} + + {fb.message} + + {new Date(fb.inserted_at).toLocaleString()} + + +
+
+
+ selectedFeedbackId && handleDelete(selectedFeedbackId)} + errorMessage={errorMessage} + /> +
+ ); +} diff --git a/src/components/feedback/add-feedback-content.tsx b/src/components/feedback/add-feedback-content.tsx new file mode 100644 index 0000000..666b83b --- /dev/null +++ b/src/components/feedback/add-feedback-content.tsx @@ -0,0 +1,83 @@ +import { useState } from "react"; +import clsx from "clsx"; +import { twMerge } from "tailwind-merge"; +import { useCreateFeedback } from "@/lib/mutations/feedback"; + +interface IAddFeedbackContent { + setModalState: (state: boolean) => void; +} + +function AddFeedbackContent({ setModalState }: IAddFeedbackContent) { + const [errorMessage, setErrorMessage] = useState(null); + + const [subject, setSubject] = useState(""); + const [message, setMessage] = useState(""); + + const createFeedback = useCreateFeedback(); + + function handleSubmit() { + setErrorMessage(null); + + createFeedback.mutate( + { + subject, + message, + }, + { + onSuccess: () => setModalState(false), + onError: () => setErrorMessage("An error occurred."), + }, + ); + } + + const isFormFilled = !!subject.trim() && !!message.trim(); + + return ( +
+
+

Subject

+ setSubject(e.target.value)} + className={ + "group flex w-full items-center justify-center rounded-md border border-gray-300 p-2 text-left select-none focus:outline-none" + } + > +

Describe the issue

+ +
+ + {errorMessage && ( +

{errorMessage}

+ )} + + +
+ ); +} + +export default AddFeedbackContent; diff --git a/src/components/feedback/banner.tsx b/src/components/feedback/banner.tsx new file mode 100644 index 0000000..aab37d0 --- /dev/null +++ b/src/components/feedback/banner.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useState } from "react"; +import { twMerge } from "tailwind-merge"; +import FeedbackModal from "./modal"; +import AddFeedbackContent from "./add-feedback-content"; +import { useGetUserInfo } from "@/lib/queries/session"; + +interface IFeedbackBanner { + bannerText: string; + linkText: string; +} + +function FeedbackBanner({ bannerText, linkText }: IFeedbackBanner) { + const [isVisible, setIsVisible] = useState(true); + const [isClosing, setIsClosing] = useState(false); + const [isModalVisible, setModalVisible] = useState(false); + + const { data: user } = useGetUserInfo(); + const type = user?.type; + if (type === "admin") return null; + + function handleCloseBanner() { + setIsClosing(true); + setTimeout(() => { + setIsVisible(false); + }, 500); + } + + if (!isVisible) return null; + + return ( +
+
+
+ + mode_comment + + + {" "} + Feedback ยท {bannerText} + + +
+ +
+ + + +
+ ); +} + +export default FeedbackBanner; diff --git a/src/components/feedback/modal.tsx b/src/components/feedback/modal.tsx new file mode 100644 index 0000000..c45204f --- /dev/null +++ b/src/components/feedback/modal.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { Fragment } from "react"; +import { + Transition, + TransitionChild, + Dialog, + DialogPanel, +} from "@headlessui/react"; + +interface IFeedbackModal { + modalState: boolean; + setModalState: (state: boolean) => void; + title: string; + children: React.ReactNode; +} + +function FeedbackModal({ + modalState, + setModalState, + children, + title, +}: IFeedbackModal) { + return ( + + setModalState(false)} + > + +
+ + +
+ + +
+

{title}

+ +
+ {children} +
+
+
+
+
+ ); +} + +export default FeedbackModal; diff --git a/src/components/sidebar-settings.tsx b/src/components/sidebar-settings.tsx index d62abc0..1064d3b 100644 --- a/src/components/sidebar-settings.tsx +++ b/src/components/sidebar-settings.tsx @@ -73,6 +73,12 @@ export default function SidebarSettings() { > + + + )} diff --git a/src/lib/feedback.ts b/src/lib/feedback.ts new file mode 100644 index 0000000..ee1a18a --- /dev/null +++ b/src/lib/feedback.ts @@ -0,0 +1,17 @@ +import { api } from "./api"; + +export async function getFeedbacks() { + const feedbacks = await api.get(`/feedbacks`); + return feedbacks.data; +} + +export async function createFeedback(data: { + subject: string; + message: string; +}) { + return await api.post(`/feedbacks`, data); +} + +export async function deleteFeedback(id: string) { + return await api.delete(`/feedbacks/${id}`); +} diff --git a/src/lib/mutations/feedback.ts b/src/lib/mutations/feedback.ts new file mode 100644 index 0000000..31cd8ff --- /dev/null +++ b/src/lib/mutations/feedback.ts @@ -0,0 +1,22 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { createFeedback, deleteFeedback } from "../feedback"; + +export function useCreateFeedback() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createFeedback, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["feedbacks"] }); + }, + }); +} + +export function useDeleteFeedback() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: deleteFeedback, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["delete_feedback"] }); + }, + }); +} diff --git a/src/lib/queries/feedback.ts b/src/lib/queries/feedback.ts new file mode 100644 index 0000000..2ceaa9b --- /dev/null +++ b/src/lib/queries/feedback.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { getFeedbacks } from "../feedback"; + +export function useGetFeedbacks() { + return useQuery({ + queryKey: ["feedbacks"], + queryFn: getFeedbacks, + refetchInterval: 3000, + }); +}