Feedbacks management go brr

This commit is contained in:
Wind-Explorer
2024-08-11 21:19:08 +08:00
parent 61bd15cbc3
commit 7f94350952
7 changed files with 343 additions and 34 deletions

View File

@@ -34,6 +34,7 @@ import ManageVoucherPage from "./pages/ManageVoucherPage";
import CreateVoucherPage from "./pages/CreateVoucherPage";
import EditVoucherPage from "./pages/EditVoucherPage";
import FeedbackPage from "./pages/FeedbackPage";
import ManageFeedbacksPage from "./pages/ManageFeedbacksPage";
function App() {
return (
@@ -91,6 +92,7 @@ function App() {
<Route path="/admin" element={<AdministratorLayout />}>
<Route index element={<AdministratorSpringboard />} />
<Route path="manage-account" element={<ManageUserAccountPage />} />
<Route path="manage-feedbacks" element={<ManageFeedbacksPage />} />
<Route path="users-management">
<Route index element={<UsersManagement />} />
</Route>

View File

@@ -63,8 +63,9 @@ export default function AdministratorNavigationPanel() {
return (
<div className="h-full">
<div
className={`fixed transition-all ${isScrolled ? "top-2" : "top-10"
} left-2`}
className={`fixed transition-all ${
isScrolled ? "top-2" : "top-10"
} left-2`}
>
<div className="bg-white rounded-full z-40">
<Button
@@ -85,17 +86,20 @@ export default function AdministratorNavigationPanel() {
{/* Panel */}
<div
className={`h-full transition-all z-50 ${panelVisible
? "scale-100 opacity-100 w-[300px] px-2"
: "w-0 scale-[98%] opacity-0 px-0"
}`}
className={`h-full transition-all z-50 ${
panelVisible
? "scale-100 opacity-100 w-[300px] px-2"
: "w-0 scale-[98%] opacity-0 px-0"
}`}
></div>
<div
className={`fixed h-full transition-all z-50 ${isScrolled ? "pb-2 -mt-8" : "pb-10"
} ${panelVisible
className={`fixed h-full transition-all z-50 ${
isScrolled ? "pb-2 -mt-8" : "pb-10"
} ${
panelVisible
? "scale-100 opacity-100 w-[300px] p-2"
: "w-0 scale-[98%] opacity-0 p-0"
}`}
}`}
>
<Card className="h-full w-full">
<div className="flex flex-col h-full">
@@ -196,7 +200,7 @@ export default function AdministratorNavigationPanel() {
<AdministratorNavigationPanelNavigationButton
text="Feedbacks"
icon={<ChatBubbleOvalLeftIcon />}
onClickRef="#"
onClickRef="manage-feedbacks"
/>
</div>
</div>

View File

@@ -542,17 +542,19 @@ export const ArrowTopRightOnSquare = () => {
export const TrashDeleteIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6">
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
);
};
@@ -563,13 +565,13 @@ export const EmailIcon = () => {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
strokeWidth="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"
/>
</svg>
@@ -582,13 +584,13 @@ export const InfoIcon = () => {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
strokeWidth="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
@@ -601,13 +603,13 @@ export const TrophyIcon = () => {
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
strokeWidth="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
strokeLinecap="round"
strokeLinejoin="round"
d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0"
/>
</svg>
@@ -651,3 +653,41 @@ export const ChevronDoubleDownIcon = () => {
</svg>
);
};
export const CheckmarkIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m4.5 12.75 6 6 9-13.5"
/>
</svg>
);
};
export const BookOpenIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"
/>
</svg>
);
};

View File

@@ -30,7 +30,7 @@ export default function FeedbackPage() {
comment: Yup.string()
.trim()
.min(1)
.max(1024)
.max(2048)
.required("Enter your comments."),
allowContact: Yup.boolean().oneOf([true, false], "please decide"),
});

View File

@@ -0,0 +1,246 @@
import { useEffect, useState } from "react";
import instance from "../security/http";
import config from "../config";
import { popErrorToast } from "../utilities";
import {
Button,
Chip,
getKeyValue,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
Textarea,
} from "@nextui-org/react";
import { BookOpenIcon, CheckmarkIcon, EmailIcon } from "../icons";
import { retrieveUserInformationById } from "../security/usersbyid";
export default function ManageFeedbacksPage() {
const [feedbacksList, setFeedbacksList] = useState<any>([]);
const [viewFeedbackModalOpened, setViewFeedbackModalOpened] = useState(false);
const [viewingFeedback, setViewingFeedback] = useState<any>();
const columns = [
{
key: "feedbackCategory",
label: "CATEGORY",
},
{
key: "subject",
label: "SUBJECT",
},
{
key: "comment",
label: "COMMENT",
},
{
key: "createdAt",
label: "SUBMITTED",
},
{
key: "actions",
label: "ACTIONS",
},
];
const populateFeedbacksList = () => {
instance
.get(`${config.serverAddress}/feedback/all`)
.then((response) => {
setFeedbacksList(response.data);
console.log(feedbacksList);
})
.catch((error) => {
popErrorToast(error);
});
};
useEffect(() => {
populateFeedbacksList();
}, []);
const viewFeedback = (feedbackId: string) => {
instance
.get(`${config.serverAddress}/feedback/${feedbackId}`)
.then((feedbackObject) => {
setViewingFeedback(feedbackObject.data);
setViewFeedbackModalOpened(true);
});
};
return (
<div className="flex flex-col gap-8 p-8">
<p className="text-4xl font-bold">Manage Feedbacks</p>
<Table aria-label="User Information Table">
<TableHeader columns={columns}>
{(column) => (
<TableColumn key={column.key}>{column.label}</TableColumn>
)}
</TableHeader>
<TableBody items={feedbacksList}>
{(feedbackEntry: any) => (
<TableRow key={feedbackEntry.id}>
{(columnKey) => (
<TableCell>
{(() => {
switch (columnKey) {
case "feedbackCategory":
let result = "";
switch (getKeyValue(feedbackEntry, columnKey)) {
case 0:
result = "Feature request";
break;
case 1:
result = "Bug report";
break;
case 2:
result = "Get in touch";
break;
default:
result = "Unknown";
break;
}
return <Chip>{result}</Chip>;
case "subject":
return (
<p className="w-max max-w-60 font-bold">
{getKeyValue(feedbackEntry, columnKey)}
</p>
);
case "comment":
return (
<div className="flex flex-row gap-4">
<p className="flex-grow line-clamp-1 max-h-4 overflow-hidden overflow-ellipsis">
{getKeyValue(feedbackEntry, columnKey)}
</p>
</div>
);
case "createdAt":
let creationDate = Date.parse(
getKeyValue(feedbackEntry, columnKey)
);
let timing = Math.floor(
(Date.now() - creationDate) / (1000 * 60 * 60 * 24)
);
return (
<p>
{timing <= 0
? "Today"
: `${timing} day${timing == 1 ? "" : "s"} ago`}
</p>
);
case "actions":
return (
<div className="flex flex-row gap-2">
<Button
size="sm"
variant="bordered"
startContent={
<div className="scale-80">
<BookOpenIcon />
</div>
}
onPress={() => {
viewFeedback(getKeyValue(feedbackEntry, "id"));
}}
>
View
</Button>
<Button
size="sm"
color="success"
variant="faded"
startContent={
<div className="scale-80">
<CheckmarkIcon />
</div>
}
onPress={() => {
instance.delete(
`${
config.serverAddress
}/feedback/${getKeyValue(
feedbackEntry,
"id"
)}`
);
window.location.reload();
}}
>
Resolve
</Button>
</div>
);
default:
return <p>{getKeyValue(feedbackEntry, columnKey)}</p>;
}
})()}
</TableCell>
)}
</TableRow>
)}
</TableBody>
</Table>
<Modal
isOpen={viewFeedbackModalOpened}
onOpenChange={setViewFeedbackModalOpened}
size="3xl"
>
<ModalContent>
{(onClose) => {
return (
viewingFeedback && (
<>
<ModalHeader>
<div className="flex flex-col gap-2">
<p>"{viewingFeedback.subject}"</p>
<p className="font-normal text-sm opacity-50">
Submitted on{" "}
{new Date(viewingFeedback.createdAt).toLocaleString()}
</p>
</div>
</ModalHeader>
<ModalBody>
<div>
<Textarea
label="Comment"
size="lg"
disabled
value={viewingFeedback.comment}
/>
</div>
</ModalBody>
<ModalFooter>
<div className="flex flex-row gap-2">
{viewingFeedback.allowContact == 1 && (
<Button
startContent={<EmailIcon />}
onPress={() => {
retrieveUserInformationById(
viewingFeedback.userId
).then((value) => {
window.location.href = `mailto:${value.email}`;
});
}}
>
Get in touch
</Button>
)}
<Button onPress={onClose}>Close</Button>
</div>
</ModalFooter>
</>
)
);
}}
</ModalContent>
</Modal>
</div>
);
}