Merged in feature-ticket-badge (pull request #36)

Added a badge with the amount of pending tickets assigned to the user

Approved-by: Tiago Ribeiro
This commit is contained in:
João Ramos
2024-02-14 00:06:27 +00:00
committed by Tiago Ribeiro
4 changed files with 220 additions and 279 deletions

View File

@@ -34,6 +34,7 @@ export default function Layout({user, children, className, navDisabled = false,
onFocusLayerMouseEnter={onFocusLayerMouseEnter} onFocusLayerMouseEnter={onFocusLayerMouseEnter}
className="-md:hidden" className="-md:hidden"
userType={user.type} userType={user.type}
userId={user.id}
/> />
<div <div
className={clsx( className={clsx(

View File

@@ -1,295 +1,206 @@
import clsx from "clsx"; import clsx from "clsx";
import { IconType } from "react-icons"; import {IconType} from "react-icons";
import { MdSpaceDashboard } from "react-icons/md"; import {MdSpaceDashboard} from "react-icons/md";
import { import {
BsFileEarmarkText, BsFileEarmarkText,
BsClockHistory, BsClockHistory,
BsPencil, BsPencil,
BsGraphUp, BsGraphUp,
BsChevronBarRight, BsChevronBarRight,
BsChevronBarLeft, BsChevronBarLeft,
BsShieldFill, BsShieldFill,
BsCloudFill, BsCloudFill,
BsCurrencyDollar, BsCurrencyDollar,
BsClipboardData, BsClipboardData,
} from "react-icons/bs"; } from "react-icons/bs";
import { RiLogoutBoxFill } from "react-icons/ri"; import {RiLogoutBoxFill} from "react-icons/ri";
import { SlPencil } from "react-icons/sl"; import {SlPencil} from "react-icons/sl";
import { FaAward } from "react-icons/fa"; import {FaAward} from "react-icons/fa";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import {useRouter} from "next/router";
import axios from "axios"; import axios from "axios";
import FocusLayer from "@/components/FocusLayer"; import FocusLayer from "@/components/FocusLayer";
import { preventNavigation } from "@/utils/navigation.disabled"; import {preventNavigation} from "@/utils/navigation.disabled";
import { useState } from "react"; import {useEffect, useState} from "react";
import usePreferencesStore from "@/stores/preferencesStore"; import usePreferencesStore from "@/stores/preferencesStore";
import { Type } from "@/interfaces/user"; import {Type} from "@/interfaces/user";
import useTicketsListener from "@/hooks/useTicketsListener";
interface Props { interface Props {
path: string; path: string;
navDisabled?: boolean; navDisabled?: boolean;
focusMode?: boolean; focusMode?: boolean;
onFocusLayerMouseEnter?: () => void; onFocusLayerMouseEnter?: () => void;
className?: string; className?: string;
userType?: Type; userType?: Type;
userId?: string;
} }
interface NavProps { interface NavProps {
Icon: IconType; Icon: IconType;
label: string; label: string;
path: string; path: string;
keyPath: string; keyPath: string;
disabled?: boolean; disabled?: boolean;
isMinimized?: boolean; isMinimized?: boolean;
badge?: number;
} }
const Nav = ({ const Nav = ({Icon, label, path, keyPath, disabled = false, isMinimized = false, badge}: NavProps) => {
Icon, return (
label, <Link
path, href={!disabled ? keyPath : ""}
keyPath, className={clsx(
disabled = false, "flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
isMinimized = false, "transition-all duration-300 ease-in-out relative",
}: NavProps) => ( disabled ? "hover:bg-mti-gray-dim cursor-not-allowed" : "hover:bg-mti-purple-light cursor-pointer",
<Link path === keyPath && "bg-mti-purple-light text-white",
href={!disabled ? keyPath : ""} isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]",
className={clsx( )}>
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white", <Icon size={24} />
"transition-all duration-300 ease-in-out", {!isMinimized && <span className="text-lg font-semibold">{label}</span>}
disabled {!!badge && badge > 0 && (
? "hover:bg-mti-gray-dim cursor-not-allowed" <div
: "hover:bg-mti-purple-light cursor-pointer", className={clsx(
path === keyPath && "bg-mti-purple-light text-white", "bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]", "transition ease-in-out duration-300",
)} isMinimized && "absolute right-0 top-0",
> )}>
<Icon size={24} /> {badge}
{!isMinimized && <span className="text-lg font-semibold">{label}</span>} </div>
</Link> )}
); </Link>
);
};
export default function Sidebar({ export default function Sidebar({path, navDisabled = false, focusMode = false, userType, onFocusLayerMouseEnter, className, userId}: Props) {
path, const router = useRouter();
navDisabled = false,
focusMode = false,
userType,
onFocusLayerMouseEnter,
className,
}: Props) {
const router = useRouter();
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [ const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]);
state.isSidebarMinimized,
state.toggleSidebarMinimized,
]);
const logout = async () => { const {totalAssignedTickets} = useTicketsListener(userId);
axios.post("/api/logout").finally(() => {
setTimeout(() => router.reload(), 500);
});
};
const disableNavigation = preventNavigation(navDisabled, focusMode); useEffect(() => console.log(totalAssignedTickets), [totalAssignedTickets]);
return ( const logout = async () => {
<section axios.post("/api/logout").finally(() => {
className={clsx( setTimeout(() => router.reload(), 500);
"relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8", });
isMinimized ? "w-fit" : "-xl:w-fit w-1/6", };
className,
)}
>
<div className="-xl:hidden flex-col gap-3 xl:flex">
<Nav
disabled={disableNavigation}
Icon={MdSpaceDashboard}
label="Dashboard"
path={path}
keyPath="/"
isMinimized={isMinimized}
/>
{(userType === "student" ||
userType === "teacher" ||
userType === "developer") && (
<>
<Nav
disabled={disableNavigation}
Icon={BsFileEarmarkText}
label="Exams"
path={path}
keyPath="/exam"
isMinimized={isMinimized}
/>
<Nav
disabled={disableNavigation}
Icon={BsPencil}
label="Exercises"
path={path}
keyPath="/exercises"
isMinimized={isMinimized}
/>
</>
)}
<Nav
disabled={disableNavigation}
Icon={BsGraphUp}
label="Stats"
path={path}
keyPath="/stats"
isMinimized={isMinimized}
/>
<Nav
disabled={disableNavigation}
Icon={BsClockHistory}
label="Record"
path={path}
keyPath="/record"
isMinimized={isMinimized}
/>
{["admin", "developer", "agent", "corporate"].includes(
userType || "",
) && (
<Nav
disabled={disableNavigation}
Icon={BsCurrencyDollar}
label="Payment Record"
path={path}
keyPath="/payment-record"
isMinimized={isMinimized}
/>
)}
{["admin", "developer", "corporate", "teacher"].includes(
userType || "",
) && (
<Nav
disabled={disableNavigation}
Icon={BsShieldFill}
label="Settings"
path={path}
keyPath="/settings"
isMinimized={isMinimized}
/>
)}
{["admin", "developer", "agent"].includes(userType || "") && (
<Nav
disabled={disableNavigation}
Icon={BsClipboardData}
label="Tickets"
path={path}
keyPath="/tickets"
isMinimized={isMinimized}
/>
)}
{userType === "developer" && (
<Nav
disabled={disableNavigation}
Icon={BsCloudFill}
label="Generation"
path={path}
keyPath="/generation"
isMinimized={isMinimized}
/>
)}
</div>
<div className="-xl:flex flex-col gap-3 xl:hidden">
<Nav
disabled={disableNavigation}
Icon={MdSpaceDashboard}
label="Dashboard"
path={path}
keyPath="/"
isMinimized={true}
/>
<Nav
disabled={disableNavigation}
Icon={BsFileEarmarkText}
label="Exams"
path={path}
keyPath="/exam"
isMinimized={true}
/>
<Nav
disabled={disableNavigation}
Icon={BsPencil}
label="Exercises"
path={path}
keyPath="/exercises"
isMinimized={true}
/>
<Nav
disabled={disableNavigation}
Icon={BsGraphUp}
label="Stats"
path={path}
keyPath="/stats"
isMinimized={true}
/>
<Nav
disabled={disableNavigation}
Icon={BsClockHistory}
label="Record"
path={path}
keyPath="/record"
isMinimized={true}
/>
{userType !== "student" && (
<Nav
disabled={disableNavigation}
Icon={BsShieldFill}
label="Settings"
path={path}
keyPath="/settings"
isMinimized={true}
/>
)}
{userType === "developer" && (
<Nav
disabled={disableNavigation}
Icon={BsCloudFill}
label="Generation"
path={path}
keyPath="/generation"
isMinimized={true}
/>
)}
</div>
<div className="fixed bottom-12 flex flex-col gap-0"> const disableNavigation = preventNavigation(navDisabled, focusMode);
<div
role="button" return (
tabIndex={1} <section
onClick={toggleMinimize} className={clsx(
className={clsx( "relative flex h-full flex-col justify-between bg-transparent px-4 py-4 pb-8",
"hover:text-mti-rose -xl:hidden flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", isMinimized ? "w-fit" : "-xl:w-fit w-1/6",
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8", className,
)} )}>
> <div className="-xl:hidden flex-col gap-3 xl:flex">
{isMinimized ? ( <Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={isMinimized} />
<BsChevronBarRight size={24} /> {(userType === "student" || userType === "teacher" || userType === "developer") && (
) : ( <>
<BsChevronBarLeft size={24} /> <Nav
)} disabled={disableNavigation}
{!isMinimized && ( Icon={BsFileEarmarkText}
<span className="text-lg font-medium">Minimize</span> label="Exams"
)} path={path}
</div> keyPath="/exam"
<div isMinimized={isMinimized}
role="button" />
tabIndex={1} <Nav
onClick={focusMode ? () => {} : logout} disabled={disableNavigation}
className={clsx( Icon={BsPencil}
"hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out", label="Exercises"
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8", path={path}
)} keyPath="/exercises"
> isMinimized={isMinimized}
<RiLogoutBoxFill size={24} /> />
{!isMinimized && ( </>
<span className="-xl:hidden text-lg font-medium">Log Out</span> )}
)} <Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
</div> <Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
</div> {["admin", "developer", "agent", "corporate"].includes(userType || "") && (
{focusMode && ( <Nav
<FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} /> disabled={disableNavigation}
)} Icon={BsCurrencyDollar}
</section> label="Payment Record"
); path={path}
keyPath="/payment-record"
isMinimized={isMinimized}
/>
)}
{["admin", "developer", "corporate", "teacher"].includes(userType || "") && (
<Nav
disabled={disableNavigation}
Icon={BsShieldFill}
label="Settings"
path={path}
keyPath="/settings"
isMinimized={isMinimized}
/>
)}
{["admin", "developer", "agent"].includes(userType || "") && (
<Nav
disabled={disableNavigation}
Icon={BsClipboardData}
label="Tickets"
path={path}
keyPath="/tickets"
isMinimized={isMinimized}
badge={totalAssignedTickets}
/>
)}
{userType === "developer" && (
<Nav
disabled={disableNavigation}
Icon={BsCloudFill}
label="Generation"
path={path}
keyPath="/generation"
isMinimized={isMinimized}
/>
)}
</div>
<div className="-xl:flex flex-col gap-3 xl:hidden">
<Nav disabled={disableNavigation} Icon={MdSpaceDashboard} label="Dashboard" path={path} keyPath="/" isMinimized={true} />
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized={true} />
<Nav disabled={disableNavigation} Icon={BsPencil} label="Exercises" path={path} keyPath="/exercises" isMinimized={true} />
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={true} />
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={true} />
{userType !== "student" && (
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized={true} />
)}
{userType === "developer" && (
<Nav disabled={disableNavigation} Icon={BsCloudFill} label="Generation" path={path} keyPath="/generation" isMinimized={true} />
)}
</div>
<div className="fixed bottom-12 flex flex-col gap-0">
<div
role="button"
tabIndex={1}
onClick={toggleMinimize}
className={clsx(
"hover:text-mti-rose -xl:hidden flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out",
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8",
)}>
{isMinimized ? <BsChevronBarRight size={24} /> : <BsChevronBarLeft size={24} />}
{!isMinimized && <span className="text-lg font-medium">Minimize</span>}
</div>
<div
role="button"
tabIndex={1}
onClick={focusMode ? () => {} : logout}
className={clsx(
"hover:text-mti-rose flex cursor-pointer items-center gap-4 rounded-full p-4 text-black transition duration-300 ease-in-out",
isMinimized ? "w-fit" : "w-full min-w-[250px] px-8",
)}>
<RiLogoutBoxFill size={24} />
{!isMinimized && <span className="-xl:hidden text-lg font-medium">Log Out</span>}
</div>
</div>
{focusMode && <FocusLayer onFocusLayerMouseEnter={onFocusLayerMouseEnter} />}
</section>
);
} }

View File

@@ -1,22 +1,22 @@
import { Ticket } from "@/interfaces/ticket"; import { Ticket } from "@/interfaces/ticket";
import { Code, Group, User } from "@/interfaces/user"; import { Code, Group, User } from "@/interfaces/user";
import axios from "axios"; import axios from "axios";
import { useEffect, useState } from "react"; import { useEffect, useState, useCallback } from "react";
export default function useTickets() { export default function useTickets() {
const [tickets, setTickets] = useState<Ticket[]>([]); const [tickets, setTickets] = useState<Ticket[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false); const [isError, setIsError] = useState(false);
const getData = () => { const getData = useCallback(() => {
setIsLoading(true); setIsLoading(true);
axios axios
.get<Ticket[]>(`/api/tickets`) .get<Ticket[]>(`/api/tickets`)
.then((response) => setTickets(response.data)) .then((response) => setTickets(response.data))
.finally(() => setIsLoading(false)); .finally(() => setIsLoading(false));
}; }, []);
useEffect(getData, []); useEffect(getData, [getData]);
return { tickets, isLoading, isError, reload: getData }; return { tickets, isLoading, isError, reload: getData };
} }

View File

@@ -0,0 +1,29 @@
import React from "react";
import useTickets from "./useTickets";
const useTicketsListener = (userId?: string) => {
const { tickets, reload } = useTickets();
React.useEffect(() => {
const intervalId = setInterval(() => {
reload();
}, 60 * 1000);
return () => clearInterval(intervalId);
}, [reload]);
if (userId) {
const assignedTickets = tickets.filter(
(ticket) => ticket.assignedTo === userId && ticket.status === "submitted"
);
return {
assignedTickets,
totalAssignedTickets: assignedTickets.length,
};
}
return {};
};
export default useTicketsListener;