Skip to content

Commit

Permalink
Merge branch 'main' into 128-Update_leaderboard_insight_page_frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
loklokyx committed Feb 26, 2025
2 parents a7c8afb + abd45c4 commit a6879f0
Show file tree
Hide file tree
Showing 83 changed files with 1,411 additions and 577 deletions.
153 changes: 104 additions & 49 deletions client/src/components/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,120 @@
import React, { useEffect, useState } from "react";
import { ArrowLeft } from "lucide-react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";

import Navbar from "@/components/navbar";
import { useTokenStore } from "@/store/token-store";
import Sidebar from "@/components/sidebar";
import { Button } from "@/components/ui/button";
import Footer from "@/components/ui/footer";
import { WaitingLoader } from "@/components/ui/loading";
import { useAuth } from "@/context/auth-provider";
import { Role } from "@/types/user";

import Sidebar from "./sidebar";
import Footer from "./ui/footer";

interface LayoutProps {
interface ProtectedPageProps {
children: React.ReactNode;
isPublic?: boolean;
requiredRoles: Role[];
}

/**
* Layout component that wraps the application with a Navbar or Sidebar based on user authentication status.
*
* @param {LayoutProps} props - The component props.
* @param {React.ReactNode} props.children - The child components to render within the layout.
*
*/
export default function Layout({ children, isPublic = false }: LayoutProps) {
const [isAuthChecked, setIsAuthChecked] = useState(false);
const { access } = useTokenStore(); // access the JWT token
const [role, setRole] = useState<string | undefined>(undefined);
export function ProtectedPage({ children, requiredRoles }: ProtectedPageProps) {
const { userRole, isLoggedIn } = useAuth();
const [isInitializing, setIsInitializing] = useState(true);
const [authState, setAuthState] = useState<
"initializing" | "authorized" | "unauthorized" | "wrong-role"
>("initializing");

useEffect(() => {
if (access?.decoded) {
const userRole = access.decoded["role"];
setRole(userRole);
}
// wait for auth to be checked before rendering
setIsAuthChecked(true);
}, [access]);
const timer = setTimeout(() => {
setIsInitializing(false);
if (!isLoggedIn || !userRole) {
setAuthState("unauthorized");
} else if (!requiredRoles.includes(userRole)) {
setAuthState("wrong-role");
} else {
setAuthState("authorized");
}
}, 0);

if (!isAuthChecked) return null;
return () => clearTimeout(timer);
}, [isLoggedIn, userRole, requiredRoles]);

if (!access || isPublic) {
return (
<div>
<Navbar />
<main>{children}</main>
<Footer />
</div>
);
}
if (isInitializing) return <WaitingLoader />;

if (!role) {
return (
<div>
<main>
<div>Failed to get user role.</div>
</main>
</div>
);
switch (authState) {
case "unauthorized":
return (
<PublicPage>
<NotAuthorizedPage />
</PublicPage>
);
case "wrong-role":
return (
<Sidebar
role={userRole.toLowerCase() as Role}
isShowBreadcrumb={userRole !== Role.STUDENT}
>
<PublicPage isNavBar={false} isFooter={false}>
<NotAuthorizedPage />
</PublicPage>
</Sidebar>
);
case "authorized":
return (
<Sidebar
role={userRole.toLowerCase() as Role}
isShowBreadcrumb={userRole !== Role.STUDENT}
>
{children}
</Sidebar>
);
default:
return <WaitingLoader />;
}
}

interface PublicPageProps {
children: React.ReactNode;
isNavBar?: boolean;
isFooter?: boolean;
}

export function PublicPage({
children,
isNavBar = true,
isFooter = true,
}: PublicPageProps) {
return (
<div>
{isNavBar ? <Navbar /> : null}
<main>{children}</main>
{isFooter ? <Footer /> : null}
</div>
);
}

function NotAuthorizedPage() {
const router = useRouter();
const { userRole, logout } = useAuth();

const handleLogout = () => {
router.push("/").then(() => logout());
};

return (
<Sidebar
role={role.toLowerCase() as Role}
isShowBreadcrumb={role === "student" ? false : true}
>
{children}
</Sidebar>
<div className="animate-fade-in flex min-h-[50vh] flex-col items-center justify-center gap-5 py-4 text-center">
<h1 className="font-black text-red-600">Access Denied</h1>
<p className="text-lg font-bold text-gray-600">
{userRole
? `Role[${userRole}] do not have permission to view this page.`
: `Unabled to identify user.`}
</p>
<Button
onClick={userRole ? () => router.push("/dashboard") : handleLogout}
variant="outline"
className="mt-6 flex animate-bounce items-center gap-2"
>
<ArrowLeft size={18} />
{userRole ? "Back to Dashboard" : "Logout"}
</Button>
</div>
);
}
6 changes: 2 additions & 4 deletions client/src/components/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { Button } from "@/components/ui/button";
import MobileNav from "@/components/ui/mobilenav";
import { LoginModal } from "@/components/ui/Users/login-modal";
import { useAuth } from "@/context/auth-provider";

import logo from "../../public/wajo_white.svg";
import styles from "../styles/modules/navbar.module.css";
import styles from "@/styles/modules/navbar.module.css";

export default function Navbar() {
const router = useRouter();
Expand All @@ -20,7 +18,7 @@ export default function Navbar() {
<div className="container mx-auto flex items-center">
<div className="flex-auto">
<Image
src={logo}
src="/wajo_white.svg"
alt="WAJO logo with white background"
width={105}
height={105}
Expand Down
15 changes: 7 additions & 8 deletions client/src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { useRouter } from "next/router";
import React from "react";

import AppSidebar from "@/components/ui/app-sidebar";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
Expand All @@ -11,14 +18,6 @@ import {
} from "@/components/ui/sidebar";
import { Role } from "@/types/user";

import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "./ui/breadcrumb";

interface LayoutProps {
children: React.ReactNode;
role: Role;
Expand Down
3 changes: 1 addition & 2 deletions client/src/components/ui/Question/add-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import "katex/dist/katex.min.css";

import * as VisuallyHidden from "@radix-ui/react-visually-hidden";

import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
Expand All @@ -10,8 +11,6 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";

import { Button } from "../button";

export default function AddModal({ children }: { children: React.ReactNode }) {
return (
<Dialog>
Expand Down
21 changes: 15 additions & 6 deletions client/src/components/ui/Question/category-data-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as React from "react";
import { Button } from "@/components/ui/button";
import DeleteModal from "@/components/ui/delete-modal";
import { SortIcon } from "@/components/ui/icon";
import { WaitingLoader } from "@/components/ui/loading";
import {
Table,
TableBody,
Expand All @@ -19,7 +20,10 @@ import { Category } from "@/types/question";

export function CategoryDataGrid({
datacontext,
isLoading,
startIdx,
onOrderingChange = () => {},
onDeleteSuccess,
}: DatagridProps<Category>) {
const router = useRouter();

Expand All @@ -30,9 +34,7 @@ export function CategoryDataGrid({
<Table className="w-full border-collapse text-left shadow-md">
<TableHeader className="bg-black text-lg font-semibold">
<TableRow className="hover:bg-muted/0">
<TableHead className={commonTableHeadClasses}>
Category Id
</TableHead>
<TableHead className={commonTableHeadClasses}>No.</TableHead>
<TableHead
className={commonTableHeadClasses}
onClick={() => onOrderingChange("genre")}
Expand All @@ -51,15 +53,17 @@ export function CategoryDataGrid({
</TableRow>
</TableHeader>
<TableBody>
{datacontext.length > 0 ? (
{!isLoading && datacontext.length > 0 ? (
datacontext.map((item, index) => (
<TableRow
key={item.id}
className={
"divide-gray-200 border-gray-50 text-sm text-black"
}
>
<TableCell className="w-0">{item.id}</TableCell>
<TableCell className="w-0">
{startIdx ? startIdx + index : item.id}
</TableCell>
<TableCell className="w-1/4">{item.genre}</TableCell>
<TableCell className="w-3/4 max-w-80 truncate">
{item.info}
Expand All @@ -73,6 +77,7 @@ export function CategoryDataGrid({
baseUrl="/questions/categories"
entity="category"
id={item.id}
onSuccess={onDeleteSuccess}
>
<Button variant={"destructive"}>Delete</Button>
</DeleteModal>
Expand All @@ -86,7 +91,11 @@ export function CategoryDataGrid({
colSpan={4}
className="py-4 text-center text-gray-500"
>
No Results Found
{isLoading ? (
<WaitingLoader className="p-0" />
) : (
"No Results Found"
)}
</TableCell>
</TableRow>
)}
Expand Down
24 changes: 17 additions & 7 deletions client/src/components/ui/Question/data-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { useRouter } from "next/router";
import * as React from "react";

import { Button } from "@/components/ui/button";
import DateTimeDisplay from "@/components/ui/date-format";
import DeleteModal from "@/components/ui/delete-modal";
import { SortIcon } from "@/components/ui/icon";
import { WaitingLoader } from "@/components/ui/loading";
import {
Table,
TableBody,
Expand All @@ -16,16 +19,16 @@ import { cn } from "@/lib/utils";
import { DatagridProps } from "@/types/data-grid";
import { Question } from "@/types/question";

import DateTimeDisplay from "../date-format";
import { SortIcon } from "../icon";

/**
* The Datagrid component is a flexible, paginated data table with sorting and navigation features.
*
* @param {DatagridProps<Question>} props - Props including datacontext (data array), onDataChange (callback for data update), and ChangePage (external control for current page).
*/
export function Datagrid({
datacontext,
isLoading,
startIdx,
onDeleteSuccess,
onOrderingChange = () => {},
}: DatagridProps<Question>) {
const router = useRouter();
Expand All @@ -37,7 +40,7 @@ export function Datagrid({
<Table className="w-full border-collapse text-left shadow-md">
<TableHeader className="bg-black text-lg font-semibold">
<TableRow className="hover:bg-muted/0">
<TableHead className={commonTableHeadClasses}>Id</TableHead>
<TableHead className={commonTableHeadClasses}>No.</TableHead>
<TableHead className={commonTableHeadClasses}>Name</TableHead>
<TableHead
className={commonTableHeadClasses}
Expand Down Expand Up @@ -68,15 +71,17 @@ export function Datagrid({
</TableRow>
</TableHeader>
<TableBody>
{datacontext.length > 0 ? (
{!isLoading && datacontext.length > 0 ? (
datacontext.map((item, index) => (
<TableRow
key={item.id}
className={
"divide-gray-200 border-gray-50 text-sm text-black"
}
>
<TableCell className="w-0">{item.id}</TableCell>
<TableCell className="w-0">
{startIdx ? startIdx + index : item.id}
</TableCell>
<TableCell className="w-1/2 max-w-80 truncate">
{item.name}
</TableCell>
Expand All @@ -97,6 +102,7 @@ export function Datagrid({
baseUrl="/questions/question-bank"
entity="question"
id={item.id}
onSuccess={onDeleteSuccess}
>
<Button variant={"destructive"}>Delete</Button>
</DeleteModal>
Expand All @@ -110,7 +116,11 @@ export function Datagrid({
colSpan={7}
className="py-4 text-center text-gray-500"
>
No Results Found
{isLoading ? (
<WaitingLoader className="p-0" />
) : (
"No Results Found"
)}
</TableCell>
</TableRow>
)}
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/Question/preview-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default function PreviewModal({
<p>
Answer: <span className="font-bold">{answer}</span>
</p>
<p>{solution}</p>
<Latex>{solution}</Latex>
</div>

<div className="mt-auto flex justify-center pb-4">
Expand Down
Loading

0 comments on commit a6879f0

Please sign in to comment.