ENCOA-316 ENCOA-317:

Refactor components to remove Layout wrapper and pass it in the App component , implemented a skeleton feedback while loading page and improved API calls related to Dashboard/User Profile
This commit is contained in:
José Marques Lima
2025-01-25 19:38:29 +00:00
parent 4d788e13b4
commit 37216e2a5a
56 changed files with 4440 additions and 2979 deletions

View File

@@ -2,17 +2,16 @@ import clsx from "clsx";
import { IconType } from "react-icons";
import { MdSpaceDashboard } from "react-icons/md";
import {
BsFileEarmarkText,
BsClockHistory,
BsPencil,
BsGraphUp,
BsChevronBarRight,
BsChevronBarLeft,
BsShieldFill,
BsCloudFill,
BsCurrencyDollar,
BsClipboardData,
BsPeople,
BsFileEarmarkText,
BsClockHistory,
BsGraphUp,
BsChevronBarRight,
BsChevronBarLeft,
BsShieldFill,
BsCloudFill,
BsCurrencyDollar,
BsClipboardData,
BsPeople,
} from "react-icons/bs";
import { CiDumbbell } from "react-icons/ci";
import { RiLogoutBoxFill } from "react-icons/ri";
@@ -24,218 +23,454 @@ import { preventNavigation } from "@/utils/navigation.disabled";
import usePreferencesStore from "@/stores/preferencesStore";
import { User } from "@/interfaces/user";
import useTicketsListener from "@/hooks/useTicketsListener";
import { checkAccess, getTypesOfUser } from "@/utils/permissions";
import { getTypesOfUser } from "@/utils/permissions";
import usePermissions from "@/hooks/usePermissions";
import { EntityWithRoles } from "@/interfaces/entity";
import { useAllowedEntities, useAllowedEntitiesSomePermissions } from "@/hooks/useEntityPermissions";
import {
useAllowedEntities,
useAllowedEntitiesSomePermissions,
} from "@/hooks/useEntityPermissions";
import { useMemo } from "react";
import { PermissionType } from "../interfaces/permissions";
interface Props {
path: string;
navDisabled?: boolean;
focusMode?: boolean;
onFocusLayerMouseEnter?: () => void;
className?: string;
user: User;
entities?: EntityWithRoles[]
path: string;
navDisabled?: boolean;
focusMode?: boolean;
onFocusLayerMouseEnter?: () => void;
className?: string;
user: User;
entities?: EntityWithRoles[];
}
interface NavProps {
Icon: IconType;
label: string;
path: string;
keyPath: string;
disabled?: boolean;
isMinimized?: boolean;
badge?: number;
Icon: IconType;
label: string;
path: string;
keyPath: string;
disabled?: boolean;
isMinimized?: boolean;
badge?: number;
}
const Nav = ({ Icon, label, path, keyPath, disabled = false, isMinimized = false, badge }: NavProps) => {
return (
<Link
href={!disabled ? keyPath : ""}
className={clsx(
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
"transition-all duration-300 ease-in-out relative",
disabled ? "hover:bg-mti-gray-dim cursor-not-allowed" : "hover:bg-mti-purple-light cursor-pointer",
path.startsWith(keyPath) && "bg-mti-purple-light text-white",
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]",
)}>
<Icon size={24} />
{!isMinimized && <span className="text-lg font-semibold">{label}</span>}
{!!badge && badge > 0 && (
<div
className={clsx(
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
"transition ease-in-out duration-300",
isMinimized && "absolute right-0 top-0",
)}>
{badge}
</div>
)}
</Link>
);
const Nav = ({
Icon,
label,
path,
keyPath,
disabled = false,
isMinimized = false,
badge,
}: NavProps) => {
return (
<Link
href={!disabled ? keyPath : ""}
className={clsx(
"flex items-center gap-4 rounded-full p-4 text-gray-500 hover:text-white",
"transition-all duration-300 ease-in-out relative",
disabled
? "hover:bg-mti-gray-dim cursor-not-allowed"
: "hover:bg-mti-purple-light cursor-pointer",
path.startsWith(keyPath) && "bg-mti-purple-light text-white",
isMinimized ? "w-fit" : "w-full min-w-[200px] px-8 2xl:min-w-[220px]"
)}
>
<Icon size={24} />
{!isMinimized && <span className="text-lg font-semibold">{label}</span>}
{!!badge && badge > 0 && (
<div
className={clsx(
"bg-mti-purple-light h-5 w-5 text-xs rounded-full flex items-center justify-center text-white",
"transition ease-in-out duration-300",
isMinimized && "absolute right-0 top-0"
)}
>
{badge}
</div>
)}
</Link>
);
};
export default function Sidebar({
path,
entities = [],
navDisabled = false,
focusMode = false,
user,
onFocusLayerMouseEnter,
className
path,
entities = [],
navDisabled = false,
focusMode = false,
user,
onFocusLayerMouseEnter,
className,
}: Props) {
const router = useRouter();
const router = useRouter();
const isAdmin = useMemo(() => ['developer', 'admin'].includes(user?.type), [user?.type])
const isAdmin = useMemo(
() => ["developer", "admin"].includes(user?.type),
[user?.type]
);
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [state.isSidebarMinimized, state.toggleSidebarMinimized]);
const [isMinimized, toggleMinimize] = usePreferencesStore((state) => [
state.isSidebarMinimized,
state.toggleSidebarMinimized,
]);
const { totalAssignedTickets } = useTicketsListener(user.id);
const { permissions } = usePermissions(user.id);
const { permissions } = usePermissions(user.id);
const entitiesAllowStatistics = useAllowedEntities(user, entities, "view_statistics")
const entitiesAllowPaymentRecord = useAllowedEntities(user, entities, "view_payment_record")
const entitiesAllowStatistics = useAllowedEntities(
user,
entities,
"view_statistics"
);
const entitiesAllowPaymentRecord = useAllowedEntities(
user,
entities,
"view_payment_record"
);
const entitiesAllowGeneration = useAllowedEntitiesSomePermissions(user, entities, [
"generate_reading", "generate_listening", "generate_writing", "generate_speaking", "generate_level"
])
const entitiesAllowGeneration = useAllowedEntitiesSomePermissions(
user,
entities,
[
"generate_reading",
"generate_listening",
"generate_writing",
"generate_speaking",
"generate_level",
]
);
const logout = async () => {
axios.post("/api/logout").finally(() => {
setTimeout(() => router.reload(), 500);
});
};
const sidebarPermissions = useMemo<{ [key: string]: boolean }>(() => {
if (user.type === "developer") {
return {
viewExams: true,
viewStats: true,
viewRecords: true,
viewTickets: true,
viewClassrooms: true,
viewSettings: true,
viewPaymentRecord: true,
viewGeneration: true,
};
}
const sidebarPermissions: { [key: string]: boolean } = {
viewExams: false,
viewStats: false,
viewRecords: false,
viewTickets: false,
viewClassrooms: false,
viewSettings: false,
viewPaymentRecord: false,
viewGeneration: false,
};
const disableNavigation = preventNavigation(navDisabled, focusMode);
if (!user || !user?.type) return sidebarPermissions;
return (
<section
className={clsx(
"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="/dashboard" isMinimized={isMinimized} />
{checkAccess(user, ["student", "teacher", "developer"], permissions, "viewExams") && (
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Practice" path={path} keyPath="/exam" isMinimized={isMinimized} />
)}
{checkAccess(user, getTypesOfUser(["agent"])) && entitiesAllowStatistics.length > 0 && (
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized={isMinimized} />
)}
{checkAccess(user, ["developer", "admin", "mastercorporate", "corporate", "teacher", "student"], permissions) && (
<Nav
disabled={disableNavigation}
Icon={BsPeople}
label="Classrooms"
path={path}
keyPath="/classrooms"
isMinimized={isMinimized}
/>
)}
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized={isMinimized} />
)}
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized={isMinimized} />
)}
{checkAccess(user, ["admin", "developer", "agent", "corporate", "mastercorporate"]) && entitiesAllowPaymentRecord.length > 0 && (
<Nav
disabled={disableNavigation}
Icon={BsCurrencyDollar}
label="Payment Record"
path={path}
keyPath="/payment-record"
isMinimized={isMinimized}
/>
)}
{checkAccess(user, ["admin", "developer", "corporate", "teacher", "mastercorporate"]) && (
<Nav
disabled={disableNavigation}
Icon={BsShieldFill}
label="Settings"
path={path}
keyPath="/settings"
isMinimized={isMinimized}
/>
)}
{checkAccess(user, ["admin", "developer", "agent"], permissions, "viewTickets") && (
<Nav
disabled={disableNavigation}
Icon={BsClipboardData}
label="Tickets"
path={path}
keyPath="/tickets"
isMinimized={isMinimized}
badge={totalAssignedTickets}
/>
)}
{checkAccess(user, ["admin", "developer", "teacher", 'corporate', 'mastercorporate'])
&& (entitiesAllowGeneration.length > 0 || isAdmin) && (
<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 />
<Nav disabled={disableNavigation} Icon={BsFileEarmarkText} label="Exams" path={path} keyPath="/exam" isMinimized />
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewStats") && (
<Nav disabled={disableNavigation} Icon={BsGraphUp} label="Stats" path={path} keyPath="/stats" isMinimized />
)}
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<Nav disabled={disableNavigation} Icon={BsClockHistory} label="Record" path={path} keyPath="/record" isMinimized />
)}
{checkAccess(user, getTypesOfUser(["agent"]), permissions, "viewRecords") && (
<Nav disabled={disableNavigation} Icon={CiDumbbell} label="Training" path={path} keyPath="/training" isMinimized />
)}
{checkAccess(user, getTypesOfUser(["student"])) && (
<Nav disabled={disableNavigation} Icon={BsShieldFill} label="Settings" path={path} keyPath="/settings" isMinimized />
)}
{entitiesAllowGeneration.length > 0 && (
<Nav
disabled={disableNavigation}
Icon={BsCloudFill}
label="Generation"
path={path}
keyPath="/generation"
isMinimized
/>
)}
</div>
const neededPermissions = permissions.reduce((acc, curr) => {
if (
["viewExams", "viewRecords", "viewTickets"].includes(curr as string)
) {
acc.push(curr);
}
return acc;
}, [] as PermissionType[]);
<div className="2xl:fixed bottom-12 flex flex-col gap-0 -2xl:mt-8">
<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>
);
if (
["student", "teacher", "developer"].includes(user.type) &&
neededPermissions.includes("viewExams")
) {
sidebarPermissions["viewExams"] = true;
}
if (
getTypesOfUser(["agent"]).includes(user.type) &&
(entitiesAllowStatistics.length > 0 ||
neededPermissions.includes("viewStats"))
) {
sidebarPermissions["viewStats"] = true;
}
if (
[
"admin",
"developer",
"teacher",
"corporate",
"mastercorporate",
].includes(user.type) &&
(entitiesAllowGeneration.length > 0 || isAdmin)
) {
sidebarPermissions["viewGeneration"] = true;
}
if (
getTypesOfUser(["agent"]).includes(user.type) &&
neededPermissions.includes("viewRecords")
) {
sidebarPermissions["viewRecords"] = true;
}
if (
["admin", "developer", "agent"].includes(user.type) &&
neededPermissions.includes("viewTickets")
) {
sidebarPermissions["viewTickets"] = true;
}
if (
[
"admin",
"mastercorporate",
"developer",
"corporate",
"teacher",
"student",
].includes(user.type)
) {
sidebarPermissions["viewClassrooms"] = true;
}
if (getTypesOfUser(["student", "agent"]).includes(user.type)) {
sidebarPermissions["viewSettings"] = true;
}
if (
["admin", "developer", "agent", "corporate", "mastercorporate"].includes(
user.type
) &&
entitiesAllowPaymentRecord.length > 0
) {
sidebarPermissions["viewPaymentRecord"] = true;
}
return sidebarPermissions;
}, [
entitiesAllowGeneration.length,
entitiesAllowPaymentRecord.length,
entitiesAllowStatistics.length,
isAdmin,
permissions,
user,
]);
const { totalAssignedTickets } = useTicketsListener(
user.id,
sidebarPermissions["viewTickets"]
);
const logout = async () => {
axios.post("/api/logout").finally(() => {
setTimeout(() => router.reload(), 500);
});
};
const disableNavigation = preventNavigation(navDisabled, focusMode);
return (
<section
className={clsx(
"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="/dashboard"
isMinimized={isMinimized}
/>
{sidebarPermissions["viewExams"] && (
<Nav
disabled={disableNavigation}
Icon={BsFileEarmarkText}
label="Practice"
path={path}
keyPath="/exam"
isMinimized={isMinimized}
/>
)}
{sidebarPermissions["viewStats"] && (
<Nav
disabled={disableNavigation}
Icon={BsGraphUp}
label="Stats"
path={path}
keyPath="/stats"
isMinimized={isMinimized}
/>
)}
{sidebarPermissions["viewClassrooms"] && (
<Nav
disabled={disableNavigation}
Icon={BsPeople}
label="Classrooms"
path={path}
keyPath="/classrooms"
isMinimized={isMinimized}
/>
)}
{sidebarPermissions["viewRecords"] && (
<Nav
disabled={disableNavigation}
Icon={BsClockHistory}
label="Record"
path={path}
keyPath="/record"
isMinimized={isMinimized}
/>
)}
{sidebarPermissions["viewRecords"] && (
<Nav
disabled={disableNavigation}
Icon={CiDumbbell}
label="Training"
path={path}
keyPath="/training"
isMinimized={isMinimized}
/>
)}
{sidebarPermissions["viewPaymentRecords"] && (
<Nav
disabled={disableNavigation}
Icon={BsCurrencyDollar}
label="Payment Record"
path={path}
keyPath="/payment-record"
isMinimized={isMinimized}
/>
)}
{sidebarPermissions["viewSettings"] && (
<Nav
disabled={disableNavigation}
Icon={BsShieldFill}
label="Settings"
path={path}
keyPath="/settings"
isMinimized={isMinimized}
/>
)}
{sidebarPermissions["viewTickets"] && (
<Nav
disabled={disableNavigation}
Icon={BsClipboardData}
label="Tickets"
path={path}
keyPath="/tickets"
isMinimized={isMinimized}
badge={totalAssignedTickets}
/>
)}
{sidebarPermissions["viewGeneration"] && (
<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
/>
<Nav
disabled={disableNavigation}
Icon={BsFileEarmarkText}
label="Exams"
path={path}
keyPath="/exam"
isMinimized
/>
{sidebarPermissions["viewStats"] && (
<Nav
disabled={disableNavigation}
Icon={BsGraphUp}
label="Stats"
path={path}
keyPath="/stats"
isMinimized
/>
)}
{sidebarPermissions["viewRecords"] && (
<Nav
disabled={disableNavigation}
Icon={BsClockHistory}
label="Record"
path={path}
keyPath="/record"
isMinimized
/>
)}
{sidebarPermissions["viewRecords"] && (
<Nav
disabled={disableNavigation}
Icon={CiDumbbell}
label="Training"
path={path}
keyPath="/training"
isMinimized
/>
)}
{sidebarPermissions["viewSettings"] && (
<Nav
disabled={disableNavigation}
Icon={BsShieldFill}
label="Settings"
path={path}
keyPath="/settings"
isMinimized
/>
)}
{sidebarPermissions["viewGeneration"] && (
<Nav
disabled={disableNavigation}
Icon={BsCloudFill}
label="Generation"
path={path}
keyPath="/generation"
isMinimized
/>
)}
</div>
<div className="2xl:fixed bottom-12 flex flex-col gap-0 -2xl:mt-8">
<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>
);
}