Scaffolded admin page

This commit is contained in:
2024-07-17 15:29:36 +08:00
parent f011f812a8
commit e6d913aa10
9 changed files with 558 additions and 6 deletions

View File

@@ -10,6 +10,7 @@ import CreatePostPage from "./pages/CreatePostPage";
import EditPostPage from "./pages/EditPostPage";
import SchedulePage from "./pages/SchedulePage";
import EventsPage from "./pages/EventsPage";
import AdministratorSpringboard from "./pages/AdministratorSpringboard";
function App() {
return (
@@ -19,13 +20,13 @@ function App() {
<Route element={<SignInPage />} path="/signin" />
<Route element={<SpringboardPage />} path="/springboard" />
<Route element={<ManageUserAccountPage />} path="/manage-account" />
<Route element={<AdministratorSpringboard />} path="/admin" />
<Route element={<CommunityPage />} path="/community" />
<Route element={<CreatePostPage />} path="/createPost" />
<Route element={<EditPostPage />} path="/editPost/:id" />
<Route element={<SchedulePage />} path="/schedule" />
<Route element={<EventsPage />} path="/events" />
</Routes>
);
}

View File

@@ -0,0 +1,212 @@
import {
Avatar,
Button,
Card,
CircularProgress,
ScrollShadow,
} from "@nextui-org/react";
import {
CalendarDaysIcon,
ChartBarIcon,
ClipboardDocumentListIcon,
ClockIcon,
TagIcon,
GiftTopIcon,
ChatBubbleOvalLeftIcon,
ChevronLeftIcon,
} from "../icons";
import EcoconnectFullLogo from "./EcoconnectFullLogo";
import { retrieveUserInformation } from "../security/users";
import { useEffect, useState } from "react";
import config from "../config";
import AdministratorNavigationPanelNavigationButton from "./AdministratorNavigationPanelNavigationButton";
import EcoconnectLogo from "./EcoconnectLogo";
export default function AdministratorNavigationPanel() {
const [userInformation, setUserInformation] = useState<any>();
const [userProfileImageURL, setUserProfileImageURL] = useState("");
const [panelVisible, setPanelVisible] = useState(true);
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
retrieveUserInformation().then((value) => {
setUserInformation(value);
setUserProfileImageURL(
`${config.serverAddress}/users/profile-image/${value.id}`
);
});
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
return (
<div className="h-full">
<div
className={`fixed transition-all top-${isScrolled ? "2" : "10"} left-2`}
>
<div className="bg-white rounded-full z-40">
<Button
isIconOnly
size="lg"
color="primary"
variant="flat"
radius="full"
className="shadow-lg"
onPress={() => {
setPanelVisible(!panelVisible);
}}
>
<EcoconnectLogo />
</Button>
</div>
</div>
{/* 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"
}`}
></div>
<div
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">
<div className="flex flex-row justify-between bg-primary-50 dark:bg-primary-950">
<div className="flex flex-col text-right p-4">
<EcoconnectFullLogo />
<p className="text-2xl text-primary-800 dark:text-primary-100 font-semibold">
administrators
</p>
</div>
<Button
onPress={() => {
setPanelVisible(!panelVisible);
}}
isIconOnly
variant="light"
className="rounded-tl-none rounded-br-none"
>
<div className="rotate-180">
<ChevronLeftIcon />
</div>
</Button>
</div>
{userInformation && (
<div className="flex flex-col h-full">
<ScrollShadow className="h-full">
<div className="flex flex-col gap-4 p-4 *:flex *:flex-col">
<div>
<p className="text-sm font-bold opacity-50 pb-2">
Events
</p>
<AdministratorNavigationPanelNavigationButton
text="Events"
icon={<CalendarDaysIcon />}
onClickRef="#"
/>
</div>
<div>
<p className="text-sm font-bold opacity-50 pb-2">
Community Forums
</p>
<AdministratorNavigationPanelNavigationButton
text="Posts"
icon={<ClipboardDocumentListIcon />}
onClickRef="#"
/>
<AdministratorNavigationPanelNavigationButton
text="Tags"
icon={<TagIcon />}
onClickRef="#"
/>
</div>
<div>
<p className="text-sm font-bold opacity-50 pb-2">
Bill Contest
</p>
<AdministratorNavigationPanelNavigationButton
text="Ranking"
icon={<ChartBarIcon />}
onClickRef="#"
/>
<AdministratorNavigationPanelNavigationButton
text="Vouchers"
icon={<GiftTopIcon />}
onClickRef="#"
/>
</div>
<div>
<p className="text-sm font-bold opacity-50 pb-2">
Karang Guni
</p>
<AdministratorNavigationPanelNavigationButton
text="Schedules"
icon={<CalendarDaysIcon />}
onClickRef="#"
/>
<AdministratorNavigationPanelNavigationButton
text="Transactions"
icon={<ClockIcon />}
onClickRef="#"
/>
</div>
<div>
<p className="text-sm font-bold opacity-50 pb-2">Users</p>
<AdministratorNavigationPanelNavigationButton
text="User Feedbacks"
icon={<ChatBubbleOvalLeftIcon />}
onClickRef="#"
/>
</div>
</div>
</ScrollShadow>
<div className="bg-primary-500 p-1">
<Button variant="light" className="h-full w-full p-2">
<div className="flex flex-row w-full justify-start">
<div className="flex flex-row w-full gap-3 *:my-auto text-white">
<Avatar src={userProfileImageURL} />
<div className="flex flex-col h-full text-left">
<p className="font-semibold">
{userInformation.firstName +
" " +
userInformation.lastName}
</p>
<p className="text-sm opacity-70">
{userInformation.email}
</p>
<p className="text-sm opacity-70">
+65 {userInformation.phoneNumber}
</p>
</div>
</div>
</div>
</Button>
</div>
</div>
)}
{!userInformation && (
<div className="w-full h-full flex flex-col justify-center">
<div className="w-full h-full flex flex-row justify-center">
<CircularProgress />
</div>
</div>
)}
</div>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,28 @@
import { Button } from "@nextui-org/react";
import React from "react";
import { useNavigate } from "react-router-dom";
export default function AdministratorNavigationPanelNavigationButton({
text,
icon,
onClickRef,
}: {
text: string;
icon: React.JSX.Element;
onClickRef: string;
}) {
const navigate = useNavigate();
return (
<Button
variant="light"
onPress={() => {
navigate(onClickRef);
}}
>
<div className="flex flex-row gap-2 w-full *:my-auto">
<div className="text-primary-500">{icon}</div>
<p>{text}</p>
</div>
</Button>
);
}

View File

@@ -0,0 +1,9 @@
export default function EcoconnectFullLogo() {
return (
<img
src="../../assets/ecoconnectFull.svg"
alt="ecoconnect logo"
className="h-6 dark:invert dark:hue-rotate-180"
/>
);
}

View File

@@ -0,0 +1,9 @@
export default function EcoconnectLogo() {
return (
<img
src="../../assets/ecoconnectLogo.svg"
alt="ecoconnect logo"
className="h-6 dark:invert dark:hue-rotate-180"
/>
);
}

View File

@@ -18,6 +18,7 @@ import config from "../config";
import { retrieveUserInformation } from "../security/users";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import EcoconnectFullLogo from "./EcoconnectFullLogo";
export default function NavigationBar() {
let [userProfileImageURL, setUserProfileImageURL] = useState("");
@@ -69,11 +70,7 @@ export default function NavigationBar() {
navigate("/");
}}
>
<img
src="../../assets/ecoconnectFull.svg"
alt="ecoconnect logo"
className="h-6 dark:invert dark:hue-rotate-180"
/>
<EcoconnectFullLogo />
</Button>
<div className="flex flex-row *:my-auto *:text-primary-800 dark:*:text-primary-100">
<Button

View File

@@ -268,3 +268,160 @@ export const ChatBubbleOvalLeftEllipsisIcon = () => {
</svg>
);
};
export const ClipboardDocumentListIcon = () => {
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="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z"
/>
</svg>
);
};
export const TagIcon = () => {
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="M9.568 3H5.25A2.25 2.25 0 0 0 3 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 6h.008v.008H6V6Z"
/>
</svg>
);
};
export const ChartBarIcon = () => {
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="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z"
/>
</svg>
);
};
export const CalendarDaysIcon = () => {
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="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z"
/>
</svg>
);
};
export const ClockIcon = () => {
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 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
</svg>
);
};
export const GiftTopIcon = () => {
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 3.75v16.5M2.25 12h19.5M6.375 17.25a4.875 4.875 0 0 0 4.875-4.875V12m6.375 5.25a4.875 4.875 0 0 1-4.875-4.875V12m-9 8.25h16.5a1.5 1.5 0 0 0 1.5-1.5V5.25a1.5 1.5 0 0 0-1.5-1.5H3.75a1.5 1.5 0 0 0-1.5 1.5v13.5a1.5 1.5 0 0 0 1.5 1.5Zm12.621-9.44c-1.409 1.41-4.242 1.061-4.242 1.061s-.349-2.833 1.06-4.242a2.25 2.25 0 0 1 3.182 3.182ZM10.773 7.63c1.409 1.409 1.06 4.242 1.06 4.242S9 12.22 7.592 10.811a2.25 2.25 0 1 1 3.182-3.182Z"
/>
</svg>
);
};
export const ChatBubbleOvalLeftIcon = () => {
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 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z"
/>
</svg>
);
};
export const BarsThreeIcon = () => {
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="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
);
};

View File

@@ -0,0 +1,28 @@
import { Toaster } from "react-hot-toast";
import SingaporeAgencyStrip from "../components/SingaporeAgencyStrip";
import AdministratorNavigationPanel from "../components/AdministratorNavigationPanel";
export default function AdministratorLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="relative flex flex-col h-full">
<SingaporeAgencyStrip />
<div className="flex flex-row h-full ">
<div className="h-full z-50">
<AdministratorNavigationPanel />
</div>
<main className="flex-grow">{children}</main>
</div>
<Toaster />
{/*
A div that becomes black in dark mode to cover white color parts
of the website when scrolling past the window's original view.
*/}
<div className="fixed -z-50 dark:bg-black inset-0 w-full h-full"></div>
</div>
);
}

View File

@@ -0,0 +1,111 @@
import { useNavigate } from "react-router-dom";
import AdministratorLayout from "../layouts/administrator";
import { useEffect, useState } from "react";
import { getTimeOfDay } from "../utilities";
import { retrieveUserInformation } from "../security/users";
import UserProfilePicture from "../components/UserProfilePicture";
import { Button, Card } from "@nextui-org/react";
import { PencilSquareIcon } from "../icons";
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
export default function AdministratorSpringboard() {
const navigate = useNavigate();
let accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
navigate("/signin");
}
let [userInformation, setUserInformation] = useState<any>();
let timeOfDay = getTimeOfDay();
let greeting = "";
if (timeOfDay === 0) {
greeting = "Good morning";
} else if (timeOfDay === 1) {
greeting = "Good afternoon";
} else if (timeOfDay === 2) {
greeting = "Good evening";
}
useEffect(() => {
retrieveUserInformation()
.then((response) => {
setUserInformation(response);
})
.catch((_) => {
navigate("/signin");
});
return;
}, []);
return (
<div>
{userInformation && (
<AdministratorLayout>
<div className="flex flex-col w-full pb-2">
<div className="flex flex-row justify-between p-8 pt-14 *:my-auto">
<div className="flex flex-col gap-3">
<p className="text-3xl font-bold">
{greeting}, {userInformation.firstName}.
</p>
<div className="flex flex-row gap-2 *:my-auto">
<p className="text-xl">A staff member of</p>
<EcoconnectFullLogo />
</div>
<p className="text-primary-500">{userInformation.email}</p>
<Button
className="w-max"
size="sm"
variant="flat"
color="primary"
startContent={
<div className="scale-80">
<PencilSquareIcon />
</div>
}
onPress={() => {
navigate("/manage-account/");
}}
>
Manage your account
</Button>
</div>
<UserProfilePicture
userId={userInformation.id}
editable={false}
/>
</div>
<div className="flex flex-row justify-stretch *:w-full *:h-56 w-full p-4 pt-0 gap-4"></div>
<Card className="w-full bg-primary-500 p-8 text-white rounded-r-none">
<div className="flex flex-col gap-4">
<p className="font-bold text-4xl">Statistics Overview</p>
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
<div className="w-60 flex flex-col justify-between">
<div className="flex flex-col">
<p className="text-2xl">User Count</p>
<p className="opacity-70">(past 30 days)</p>
</div>
<p className="text-lg">
Total: <span className="font-bold">2139</span> users
</p>
</div>
<div className="w-full h-full bg-white rounded-xl">
{/* TODO: Graph */}
<p className="text-black">GRAPH HERE</p>
</div>
</div>
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
<div className="w-60 flex flex-col justify-between">
<p className="text-2xl">Population Distribution</p>
</div>
<div className="w-full h-full bg-white rounded-xl">
{/* TODO: Graph */}
<p className="text-black">GRAPH HERE</p>
</div>
</div>
</div>
</Card>
</div>
</AdministratorLayout>
)}
</div>
);
}