Started the redesign of the dashboard

This commit is contained in:
Tiago Ribeiro
2023-05-26 19:46:50 +01:00
parent 2b34bf8f0b
commit 9ed3672cb6
8 changed files with 99 additions and 100 deletions

View File

@@ -34,6 +34,7 @@
"react-chartjs-2": "^5.2.0", "react-chartjs-2": "^5.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-firebase-hooks": "^5.1.1", "react-firebase-hooks": "^5.1.1",
"react-icons": "^4.8.0",
"react-lineto": "^3.3.0", "react-lineto": "^3.3.0",
"react-media-recorder": "^1.6.6", "react-media-recorder": "^1.6.6",
"react-player": "^2.12.0", "react-player": "^2.12.0",

View File

@@ -1,77 +1,22 @@
import {Type} from "@/interfaces/user"; import {User} from "@/interfaces/user";
import axios from "axios"; import {Avatar} from "primereact/avatar";
import Link from "next/link";
import {useRouter} from "next/router";
import {Button} from "primereact/button";
import {Menubar} from "primereact/menubar";
import {MenuItem} from "primereact/menuitem";
interface Props { interface Props {
profilePicture: string; user: User;
userType: Type;
timer?: number;
showExamEnd?: boolean;
} }
/* eslint-disable @next/next/no-img-element */ /* eslint-disable @next/next/no-img-element */
export default function Navbar({profilePicture, userType, timer, showExamEnd = false}: Props) { export default function Navbar({user}: Props) {
const router = useRouter();
const logout = async () => {
axios.post("/api/logout").finally(() => {
router.push("/login");
});
};
const items: MenuItem[] = [
{
label: "Home",
icon: "pi pi-fw pi-home",
url: "/",
},
{
label: "Account",
icon: "pi pi-fw pi-user",
url: "/profile",
},
{
label: "Exam",
icon: "pi pi-fw pi-plus-circle",
url: "/exam",
},
{
label: "Users",
icon: "pi pi-fw pi-users",
items: [
...(userType === "student" ? [] : [{label: "List", icon: "pi pi-fw pi-users", url: "/users"}]),
{label: "Stats", icon: "pi pi-fw pi-chart-pie", url: "/stats"},
{label: "History", icon: "pi pi-fw pi-history", url: "/history"},
],
},
{
label: "Logout",
icon: "pi pi-fw pi-power-off",
command: logout,
},
];
const endTimer = timer && (
<span className="pr-2 font-semibold">
{Math.floor(timer / 60) < 10 ? "0" : ""}
{Math.floor(timer / 60)}:{timer % 60 < 10 ? "0" : ""}
{timer % 60}
</span>
);
const endNewExam = (
<Link href="/exam" className="pr-2">
<Button text label="Exam" severity="secondary" size="small" />
</Link>
);
return ( return (
<div className="bg-neutral-100 z-10 w-full p-2"> <header className="w-full bg-transparent py-4 gap-2 flex items-center">
<Menubar model={items} end={showExamEnd ? endNewExam : endTimer} /> <h1 className="font-bold text-2xl w-1/6 px-8">eCrop</h1>
</div> <div className="flex justify-between w-5/6 mr-8">
<input type="text" placeholder="Search..." className="rounded-full py-3 px-6 shadow-md outline-none" />
<div className="flex gap-3 items-center justify-end">
<Avatar size="normal" label={user.name.slice(0, 1)} shape="circle" />
<span className="text-right">{user.name}</span>
</div>
</div>
</header>
); );
} }

View File

@@ -0,0 +1,66 @@
import clsx from "clsx";
import {IconType} from "react-icons";
import {MdSpaceDashboard} from "react-icons/md";
import {BsFileEarmarkText, BsClockHistory} from "react-icons/bs";
import {RiLogoutBoxFill} from "react-icons/ri";
import {SlPencil} from "react-icons/sl";
import {FaAward} from "react-icons/fa";
import Link from "next/link";
import {useRouter} from "next/router";
import axios from "axios";
interface Props {
path: string;
}
interface NavProps {
Icon: IconType;
label: string;
path: string;
keyPath: string;
}
const Nav = ({Icon, label, path, keyPath}: NavProps) => (
<Link
href={keyPath}
className={clsx(
"p-4 px-8 rounded-full flex gap-4 items-center cursor-pointer text-black hover:bg-mti-green hover:text-white transition duration-300 ease-in-out",
path === keyPath && "bg-mti-green text-white",
)}>
<Icon size={20} />
<span>{label}</span>
</Link>
);
export default function Sidebar({path}: Props) {
const router = useRouter();
const logout = async () => {
axios.post("/api/logout").finally(() => {
router.push("/login");
});
};
return (
<section className="h-full flex bg-transparent flex-col justify-between w-1/6 px-4">
<div className="flex flex-col gap-3">
<Nav Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" />
<Nav Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" />
<Nav Icon={SlPencil} label="Exercises" path={path} keyPath="/exercise" />
<Nav Icon={FaAward} label="Score" path={path} keyPath="/score" />
<Nav Icon={BsClockHistory} label="Record" path={path} keyPath="/record" />
</div>
<div
role="button"
tabIndex={1}
onClick={logout}
className={clsx(
"p-4 px-8 rounded-full flex gap-4 items-center cursor-pointer text-black hover:text-mti-orange transition duration-300 ease-in-out",
)}>
<RiLogoutBoxFill size={20} />
<span>Log Out</span>
</div>
</section>
);
}

View File

@@ -208,10 +208,7 @@ export default function Page() {
</Head> </Head>
<ToastContainer /> <ToastContainer />
{user && ( {user && (
<main className="w-full h-full min-h-[100vh] flex flex-col items-center bg-neutral-100 text-black pb-4 gap-4"> <main className="w-full h-full min-h-[100vh] flex flex-col items-center bg-neutral-100 text-black pb-4 gap-4">{renderScreen()}</main>
<Navbar userType={user.type} profilePicture={user.profilePicture} timer={exam ? timer : undefined} />
{renderScreen()}
</main>
)} )}
</> </>
); );

View File

@@ -152,7 +152,6 @@ export default function History({user}: {user: User}) {
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
<main className="w-full h-full min-h-[100vh] flex flex-col bg-neutral-100 text-black"> <main className="w-full h-full min-h-[100vh] flex flex-col bg-neutral-100 text-black">
<Navbar userType={user.type} profilePicture={user.profilePicture} />
<div className="w-fit self-center"> <div className="w-fit self-center">
{!isUsersLoading && user.type !== "student" && ( {!isUsersLoading && user.type !== "student" && (
<Dropdown value={selectedUser} options={users} optionLabel="name" onChange={(e) => setSelectedUser(e.target.value)} /> <Dropdown value={selectedUser} options={users} optionLabel="name" onChange={(e) => setSelectedUser(e.target.value)} />

View File

@@ -11,6 +11,7 @@ import useStats from "@/hooks/useStats";
import {averageScore, formatModuleTotalStats, totalExams} from "@/utils/stats"; import {averageScore, formatModuleTotalStats, totalExams} from "@/utils/stats";
import {Divider} from "primereact/divider"; import {Divider} from "primereact/divider";
import useUser from "@/hooks/useUser"; import useUser from "@/hooks/useUser";
import Sidebar from "@/components/Sidebar";
export const getServerSideProps = withIronSessionSsr(({req, res}) => { export const getServerSideProps = withIronSessionSsr(({req, res}) => {
const user = req.session.user; const user = req.session.user;
@@ -53,32 +54,11 @@ export default function Home() {
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
</Head> </Head>
{user && ( {user && (
<main className="w-full h-full min-h-[100vh] flex flex-col items-center bg-neutral-100 text-black"> <main className="w-full h-full min-h-[100vh] flex flex-col bg-mti-gray text-black">
<Navbar userType={user.type} profilePicture={user.profilePicture} showExamEnd={showEndExam} /> <Navbar user={user} />
<div className="w-full h-full p-4 relative flex flex-col gap-8"> <div className="h-full w-full flex py-4 gap-2">
<section className="h-full w-full flex lg:gap-8 flex-col lg:flex-row justify-center md:justify-start md:items-start"> <Sidebar path="/" />
<section className="w-full h-full flex items-center"> <div className="w-5/6 mr-8 bg-white shadow-md rounded-2xl"></div>
<ProfileCard user={user} className="text-black self-start" />
</section>
{windowWidth <= 960 && <Divider />}
<div className="flex flex-col w-full gap-4">
<span className="font-bold text-2xl">Statistics</span>
{!isLoading && stats && (
<div className="text-neutral-600 flex flex-wrap gap-2 md:gap-4 w-full justify-between md:justify-start">
<div className="bg-white p-4 rounded-xl drop-shadow-xl flex flex-col gap-2 md:gap-4 w-full">
<span className="font-bold text-xl">Exams: {totalExams(stats)}</span>
<span className="font-bold text-xl">Exercises: {stats.length}</span>
<span className="font-bold text-xl">Average Score: {averageScore(stats)}%</span>
</div>
</div>
)}
</div>
</section>
{!isLoading && stats && (
<section className="w-full lg:w-1/3 h-full flex items-center justify-center">
<SingleDatasetChart type="polarArea" data={formatModuleTotalStats(stats)} title="Exams per Module" />
</section>
)}
</div> </div>
</main> </main>
)} )}

View File

@@ -4,6 +4,12 @@ module.exports = {
theme: { theme: {
extend: { extend: {
colors: { colors: {
mti: {
orange: "#FF6000",
green: "#51C21A",
blue: "#007FF8",
gray: "#F9F9F9",
},
ielts: { ielts: {
reading: {DEFAULT: "#FF6384", transparent: "rgba(255, 99, 132, 0.5)"}, reading: {DEFAULT: "#FF6384", transparent: "rgba(255, 99, 132, 0.5)"},
listening: {DEFAULT: "#36A2EB", transparent: "rgba(54, 162, 235, 0.5)"}, listening: {DEFAULT: "#36A2EB", transparent: "rgba(54, 162, 235, 0.5)"},

View File

@@ -3120,6 +3120,11 @@ react-firebase-hooks@^5.1.1:
resolved "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz" resolved "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz"
integrity sha512-y2UpWs82xs+39q5Rc/wq316ca52QsC0n8m801V+yM4IC4hbfOL4yQPVSh7w+ydstdvjN9F+lvs1WrO2VYxpmdA== integrity sha512-y2UpWs82xs+39q5Rc/wq316ca52QsC0n8m801V+yM4IC4hbfOL4yQPVSh7w+ydstdvjN9F+lvs1WrO2VYxpmdA==
react-icons@^4.8.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==
react-is@^16.13.1, react-is@^16.8.1: react-is@^16.13.1, react-is@^16.8.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"