From 89342d3b4c58694aa63c80dc18b84cdb8484153e Mon Sep 17 00:00:00 2001
From: Lok Yx
Date: Tue, 14 Jan 2025 21:46:50 +0000
Subject: [PATCH 1/3] feat(admin): add UserManagement components, improve Admin
Dashboard features
- feat(admin): implement `UserManagement` components for managing schools, users, and teams
- feat(admin): add paginated data table (`data-grid`) and multiple-insert form (`data-table-form`)
- feat(ui): add `SelectRole` and `SelectSchool` components for dropdown functionality
- fix(sidebar): resolve issue with active tab not working correctly
- refactor: change file naming in `/components/ui/` to follow PascalCase convention
- refactor: move pagination props to a separate file for better organization
---
.../leaderboard-list.tsx | 0
.../src/components/ui/Question/data-grid.tsx | 9 +-
client/src/components/ui/Users/data-grid.tsx | 95 ++++++++
.../components/ui/Users/data-table-form.tsx | 220 ++++++++++++++++++
.../ui/{user => Users}/login-modal.tsx | 0
.../components/ui/Users/school-data-grid.tsx | 106 +++++++++
.../ui/Users/school-data-table-form.tsx | 121 ++++++++++
.../src/components/ui/Users/select-school.tsx | 58 +++++
.../components/ui/Users/team-data-grid.tsx | 110 +++++++++
.../ui/Users/team-data-table-form.tsx | 176 ++++++++++++++
client/src/components/ui/app-sidebar.tsx | 9 +-
client/src/components/ui/select-role.tsx | 39 ++++
client/src/pages/api/users/index.ts | 61 +++++
client/src/pages/api/users/school.ts | 58 +++++
client/src/pages/api/users/team.ts | 64 +++++
client/src/pages/index.tsx | 5 +-
client/src/pages/leaderboard/index.tsx | 2 +-
client/src/pages/users/create.tsx | 5 +
client/src/pages/users/create_school.tsx | 5 +
client/src/pages/users/create_team.tsx | 5 +
client/src/pages/users/index.tsx | 74 ++++++
client/src/pages/users/school.tsx | 74 ++++++
client/src/pages/users/team.tsx | 74 ++++++
client/src/types/app-sidebar.ts | 6 +-
client/src/types/data-grid.ts | 47 ++++
client/src/types/question.ts | 48 ----
client/src/types/school.ts | 11 +
client/src/types/team.ts | 15 ++
client/src/types/user.ts | 13 +-
29 files changed, 1444 insertions(+), 66 deletions(-)
rename client/src/components/ui/{leaderboard => Leaderboard}/leaderboard-list.tsx (100%)
create mode 100644 client/src/components/ui/Users/data-grid.tsx
create mode 100644 client/src/components/ui/Users/data-table-form.tsx
rename client/src/components/ui/{user => Users}/login-modal.tsx (100%)
create mode 100644 client/src/components/ui/Users/school-data-grid.tsx
create mode 100644 client/src/components/ui/Users/school-data-table-form.tsx
create mode 100644 client/src/components/ui/Users/select-school.tsx
create mode 100644 client/src/components/ui/Users/team-data-grid.tsx
create mode 100644 client/src/components/ui/Users/team-data-table-form.tsx
create mode 100644 client/src/components/ui/select-role.tsx
create mode 100644 client/src/pages/api/users/index.ts
create mode 100644 client/src/pages/api/users/school.ts
create mode 100644 client/src/pages/api/users/team.ts
create mode 100644 client/src/pages/users/create.tsx
create mode 100644 client/src/pages/users/create_school.tsx
create mode 100644 client/src/pages/users/create_team.tsx
create mode 100644 client/src/pages/users/index.tsx
create mode 100644 client/src/pages/users/school.tsx
create mode 100644 client/src/pages/users/team.tsx
create mode 100644 client/src/types/data-grid.ts
create mode 100644 client/src/types/school.ts
create mode 100644 client/src/types/team.ts
diff --git a/client/src/components/ui/leaderboard/leaderboard-list.tsx b/client/src/components/ui/Leaderboard/leaderboard-list.tsx
similarity index 100%
rename from client/src/components/ui/leaderboard/leaderboard-list.tsx
rename to client/src/components/ui/Leaderboard/leaderboard-list.tsx
diff --git a/client/src/components/ui/Question/data-grid.tsx b/client/src/components/ui/Question/data-grid.tsx
index 9581cbe..bc9778a 100644
--- a/client/src/components/ui/Question/data-grid.tsx
+++ b/client/src/components/ui/Question/data-grid.tsx
@@ -1,5 +1,7 @@
import React, { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Pagination } from "@/components/ui/pagination";
import {
Table,
TableBody,
@@ -9,19 +11,16 @@ import {
TableRow,
} from "@/components/ui/table";
-import { Button } from "../button";
-import { Pagination } from "../pagination";
-
/**
* The Datagrid component is a flexible, paginated data table with sorting and navigation features.
*
- * @param {DatagridProps} props - Props including datacontext (data array), onDataChange (callback for data update), and ChangePage (external control for current page).
+ * @param {DatagridProps} props - Props including datacontext (data array), onDataChange (callback for data update), and ChangePage (external control for current page).
*/
export function Datagrid({
datacontext,
onDataChange,
changePage,
-}: DatagridProps) {
+}: DatagridProps) {
// State to track sorting direction
const [isAscending, setIsAscending] = useState(true);
// State for the current page number
diff --git a/client/src/components/ui/Users/data-grid.tsx b/client/src/components/ui/Users/data-grid.tsx
new file mode 100644
index 0000000..cf427b9
--- /dev/null
+++ b/client/src/components/ui/Users/data-grid.tsx
@@ -0,0 +1,95 @@
+import React, { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { Pagination } from "@/components/ui/pagination";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { cn } from "@/lib/utils";
+import { User } from "@/types/user";
+
+export function DataGrid({
+ datacontext,
+ onDataChange,
+ changePage,
+}: DatagridProps) {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [paddedData, setPaddedData] = useState([]);
+ const itemsPerPage = 5;
+ const totalPages = Math.ceil(datacontext.length / itemsPerPage);
+
+ const handlePageChange = (page: number) => {
+ if (page >= 1 && page <= totalPages) {
+ setCurrentPage(page);
+ }
+ };
+
+ useEffect(() => {
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentData = datacontext.slice(indexOfFirstItem, indexOfLastItem);
+
+ const updatedPaddedData = [...currentData];
+ while (updatedPaddedData.length < itemsPerPage) {
+ updatedPaddedData.push({} as User);
+ }
+
+ setPaddedData(updatedPaddedData);
+ }, [datacontext, currentPage]);
+
+ useEffect(() => {
+ setCurrentPage(changePage);
+ }, [datacontext]);
+
+ const commonTableHeadClasses = "w-auto text-white text-nowrap";
+ return (
+
+
+
+
+
+ User Id
+
+ User Name
+ User Role
+ School
+
+ Actions
+
+
+
+
+ {paddedData.map((item, index) => (
+
+ {item.id}
+ {item.username}
+ {item.role}
+ {item.school}
+
+
+
+
+
+
+
+ ))}
+
+
+
+
handlePageChange(page)}
+ className="mr-20 mt-5 flex justify-end"
+ />
+
+ );
+}
diff --git a/client/src/components/ui/Users/data-table-form.tsx b/client/src/components/ui/Users/data-table-form.tsx
new file mode 100644
index 0000000..8dcebdc
--- /dev/null
+++ b/client/src/components/ui/Users/data-table-form.tsx
@@ -0,0 +1,220 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useFieldArray, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import { SelectRole } from "@/components/ui/select-role";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { SelectSchool } from "@/components/ui/Users/select-school";
+import { cn } from "@/lib/utils";
+import { createUserSchema } from "@/types/user";
+
+type User = z.infer;
+
+export default function DataTableForm() {
+ const defaultUser = {
+ username: "",
+ password: "",
+ email: "",
+ } as User;
+
+ const createUserForm = useForm<{
+ users: User[];
+ }>({
+ resolver: zodResolver(z.object({ users: z.array(createUserSchema) })),
+ defaultValues: { users: [defaultUser] },
+ });
+
+ const { fields, append, remove } = useFieldArray({
+ control: createUserForm.control,
+ name: "users",
+ });
+
+ const onSubmit = (data: { users: User[] }) => {
+ console.log("Submitted data:", data.users);
+ };
+
+ const commonTableHeadClasses = "w-auto text-white text-nowrap";
+ return (
+
+ );
+}
diff --git a/client/src/components/ui/user/login-modal.tsx b/client/src/components/ui/Users/login-modal.tsx
similarity index 100%
rename from client/src/components/ui/user/login-modal.tsx
rename to client/src/components/ui/Users/login-modal.tsx
diff --git a/client/src/components/ui/Users/school-data-grid.tsx b/client/src/components/ui/Users/school-data-grid.tsx
new file mode 100644
index 0000000..55570fa
--- /dev/null
+++ b/client/src/components/ui/Users/school-data-grid.tsx
@@ -0,0 +1,106 @@
+import React, { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { Pagination } from "@/components/ui/pagination";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { cn } from "@/lib/utils";
+import { School } from "@/types/school";
+
+export function SchoolDataGrid({
+ datacontext,
+ onDataChange,
+ changePage,
+}: DatagridProps) {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [paddedData, setPaddedData] = useState([]);
+ const itemsPerPage = 5;
+ const totalPages = Math.ceil(datacontext.length / itemsPerPage);
+
+ const handlePageChange = (page: number) => {
+ if (page >= 1 && page <= totalPages) {
+ setCurrentPage(page);
+ }
+ };
+
+ useEffect(() => {
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentData = datacontext.slice(indexOfFirstItem, indexOfLastItem);
+
+ const updatedPaddedData = [...currentData];
+ while (updatedPaddedData.length < itemsPerPage) {
+ updatedPaddedData.push({} as School);
+ }
+
+ setPaddedData(updatedPaddedData);
+ }, [datacontext, currentPage]);
+
+ useEffect(() => {
+ setCurrentPage(changePage);
+ }, [datacontext]);
+
+ const commonTableHeadClasses = "w-auto text-white text-nowrap";
+ return (
+
+
+
+
+
+ School Id
+
+
+ School Name
+
+ Created On
+
+ Actions
+
+
+
+
+ {paddedData.map((item, index) => (
+
+ {item.id}
+ {item.name}
+
+ {item.time_created ? (
+ <>
+
+ {new Date(item.time_created).toLocaleDateString()}
+
+
+ {new Date(item.time_created).toLocaleTimeString()}
+
+ >
+ ) : null}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
handlePageChange(page)}
+ className="mr-20 mt-5 flex justify-end"
+ />
+
+ );
+}
diff --git a/client/src/components/ui/Users/school-data-table-form.tsx b/client/src/components/ui/Users/school-data-table-form.tsx
new file mode 100644
index 0000000..c9dff9d
--- /dev/null
+++ b/client/src/components/ui/Users/school-data-table-form.tsx
@@ -0,0 +1,121 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useFieldArray, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { cn } from "@/lib/utils";
+import { createSchoolSchema } from "@/types/school";
+
+type School = z.infer;
+
+export default function SchoolDataTableForm() {
+ const defaultSchool = {
+ name: "",
+ } as School;
+
+ const createSchoolForm = useForm<{
+ schools: School[];
+ }>({
+ resolver: zodResolver(z.object({ schools: z.array(createSchoolSchema) })),
+ defaultValues: { schools: [defaultSchool] },
+ });
+
+ const { fields, append, remove } = useFieldArray({
+ control: createSchoolForm.control,
+ name: "schools",
+ });
+
+ const onSubmit = (data: { schools: School[] }) => {
+ console.log("Submitted data:", data.schools);
+ };
+
+ const commonTableHeadClasses = "w-auto text-white text-nowrap";
+ return (
+
+ );
+}
diff --git a/client/src/components/ui/Users/select-school.tsx b/client/src/components/ui/Users/select-school.tsx
new file mode 100644
index 0000000..dca4286
--- /dev/null
+++ b/client/src/components/ui/Users/select-school.tsx
@@ -0,0 +1,58 @@
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { useFetchData } from "@/hooks/use-fetch-data";
+import { cn } from "@/lib/utils";
+import { School } from "@/types/school";
+
+type Props = {
+ selectedId: number | undefined;
+ onChange: (id: number) => void;
+ className?: string;
+};
+
+export function SelectSchool({ selectedId, onChange, className }: Props) {
+ const {
+ data: schools,
+ isPending,
+ isError,
+ } = useFetchData({
+ queryKey: ["users.school.list"],
+ endpoint: "/users/school",
+ });
+
+ const onValueChange = (value: string) => {
+ const parsed = parseInt(value);
+ if (parsed) {
+ onChange(parsed);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/components/ui/Users/team-data-grid.tsx b/client/src/components/ui/Users/team-data-grid.tsx
new file mode 100644
index 0000000..d32be2c
--- /dev/null
+++ b/client/src/components/ui/Users/team-data-grid.tsx
@@ -0,0 +1,110 @@
+import React, { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { Pagination } from "@/components/ui/pagination";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { cn } from "@/lib/utils";
+import { Team } from "@/types/team";
+
+export function TeamDataGrid({
+ datacontext,
+ onDataChange,
+ changePage,
+}: DatagridProps) {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [paddedData, setPaddedData] = useState([]);
+ const itemsPerPage = 5;
+ const totalPages = Math.ceil(datacontext.length / itemsPerPage);
+
+ const handlePageChange = (page: number) => {
+ if (page >= 1 && page <= totalPages) {
+ setCurrentPage(page);
+ }
+ };
+
+ useEffect(() => {
+ const indexOfLastItem = currentPage * itemsPerPage;
+ const indexOfFirstItem = indexOfLastItem - itemsPerPage;
+ const currentData = datacontext.slice(indexOfFirstItem, indexOfLastItem);
+
+ const updatedPaddedData = [...currentData];
+ while (updatedPaddedData.length < itemsPerPage) {
+ updatedPaddedData.push({} as Team);
+ }
+
+ setPaddedData(updatedPaddedData);
+ }, [datacontext, currentPage]);
+
+ useEffect(() => {
+ setCurrentPage(changePage);
+ }, [datacontext]);
+
+ const commonTableHeadClasses = "w-auto text-white text-nowrap";
+ return (
+
+
+
+
+
+ Team Id
+
+ Team Name
+ School
+
+ Description
+
+ Created On
+
+ Actions
+
+
+
+
+ {paddedData.map((item, index) => (
+
+ {item.id}
+ {item.name}
+ {item.school}
+ {item.description}
+
+ {item.time_created ? (
+ <>
+
+ {new Date(item.time_created).toLocaleDateString()}
+
+
+ {new Date(item.time_created).toLocaleTimeString()}
+
+ >
+ ) : null}
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
handlePageChange(page)}
+ className="mr-20 mt-5 flex justify-end"
+ />
+
+ );
+}
diff --git a/client/src/components/ui/Users/team-data-table-form.tsx b/client/src/components/ui/Users/team-data-table-form.tsx
new file mode 100644
index 0000000..122e936
--- /dev/null
+++ b/client/src/components/ui/Users/team-data-table-form.tsx
@@ -0,0 +1,176 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useFieldArray, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { Button } from "@/components/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormMessage,
+} from "@/components/ui/form";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { Textarea } from "@/components/ui/textarea";
+import { SelectSchool } from "@/components/ui/Users/select-school";
+import { cn } from "@/lib/utils";
+import { createTeamSchema } from "@/types/team";
+
+type Team = z.infer;
+
+export default function TeamDataTableForm() {
+ const defaultTeam = {
+ name: "",
+ description: "",
+ } as Team;
+
+ const createTeamForm = useForm<{
+ teams: Team[];
+ }>({
+ resolver: zodResolver(z.object({ teams: z.array(createTeamSchema) })),
+ defaultValues: { teams: [defaultTeam] },
+ });
+
+ const { fields, append, remove } = useFieldArray({
+ control: createTeamForm.control,
+ name: "teams",
+ });
+
+ const onSubmit = (data: { teams: Team[] }) => {
+ console.log("Submitted data:", data.teams);
+ };
+
+ const commonTableHeadClasses = "w-auto text-white text-nowrap";
+ return (
+
+ );
+}
diff --git a/client/src/components/ui/app-sidebar.tsx b/client/src/components/ui/app-sidebar.tsx
index cb3b030..8b403d9 100644
--- a/client/src/components/ui/app-sidebar.tsx
+++ b/client/src/components/ui/app-sidebar.tsx
@@ -52,12 +52,11 @@ export function AppSidebar({ Role, ...props }: AppSidebarProps) {
const roleNavData = navData[Role];
roleNavData.forEach((section) => {
for (const item of section.items) {
- item.isActive = new RegExp(item.url.replace(/\[.*?\]/g, ".*")).test(
- router.pathname,
- );
- if (item.isActive) {
+ const regex = new RegExp(`^${item.url.replace(/\[.*?\]/g, ".*")}$`);
+ item.isActive = regex.test(router.pathname);
+
+ if (item.isActive && !section.isActive) {
section.isActive = true;
- break; // exit the loop
}
}
});
diff --git a/client/src/components/ui/select-role.tsx b/client/src/components/ui/select-role.tsx
new file mode 100644
index 0000000..9da3a2e
--- /dev/null
+++ b/client/src/components/ui/select-role.tsx
@@ -0,0 +1,39 @@
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { cn } from "@/lib/utils";
+import { RoleEnum } from "@/types/user";
+
+type Props = {
+ selectedRole: string | undefined;
+ onChange: (role: string) => void;
+ className?: string;
+};
+
+export function SelectRole({ selectedRole, onChange, className }: Props) {
+ const onValueChange = (value: string) => {
+ onChange(value);
+ };
+
+ return (
+
+ );
+}
diff --git a/client/src/pages/api/users/index.ts b/client/src/pages/api/users/index.ts
new file mode 100644
index 0000000..c702536
--- /dev/null
+++ b/client/src/pages/api/users/index.ts
@@ -0,0 +1,61 @@
+/**
+ * API Route Handler for fetching user data.
+ *
+ * This handler responds to API requests by returning a mock list of users.
+ * It simulates backend behavior and serves static user data for demonstration purposes.
+ *
+ * @fileoverview API endpoint located at `/api/users` for fetching mock user data.
+ *
+ * @module /api/users
+ * @see {@link https://nextjs.org/docs/pages/building-your-application/routing/api-routes | Next.js API Routes Documentation}
+ */
+
+import { NextApiRequest, NextApiResponse } from "next";
+
+import { User } from "@/types/user";
+
+/**
+ * Mock data representing user entries.
+ *
+ * @constant {Partial[]}
+ */
+const mockUsers: Partial[] = [];
+for (let i = 0; i < 9; i++) {
+ mockUsers.push(
+ {
+ id: i * 3 + 1,
+ username: `adminMaster${i + 1}`,
+ role: "admin",
+ school: "Greenfield High",
+ },
+ {
+ id: i * 3 + 2,
+ username: `mathPro${i + 1}`,
+ role: "teacher",
+ school: "Westwood Academy",
+ },
+ {
+ id: i * 3 + 3,
+ username: `scienceGeek${i + 1}`,
+ role: "student",
+ school: "Northside School",
+ },
+ );
+}
+
+/**
+ * Handles API requests to fetch users data.
+ *
+ * @function
+ * @name handler
+ * @param {NextApiRequest} _req - The API request object. This parameter is not used in this handler.
+ * @param {NextApiResponse} res - The API response object to return the user data.
+ *
+ * @returns {void} Responds with a status code of 200 and the mock user data in JSON format.
+ */
+export default function handler(
+ _req: NextApiRequest,
+ res: NextApiResponse,
+): void {
+ res.status(200).json(mockUsers as User[]);
+}
diff --git a/client/src/pages/api/users/school.ts b/client/src/pages/api/users/school.ts
new file mode 100644
index 0000000..34b91ec
--- /dev/null
+++ b/client/src/pages/api/users/school.ts
@@ -0,0 +1,58 @@
+/**
+ * API Route Handler for fetching school data.
+ *
+ * This handler responds to API requests by returning a mock list of schools.
+ * It simulates backend behavior and serves static school data for demonstration purposes.
+ *
+ * @fileoverview API endpoint located at `/api/users/school` for fetching mock school data.
+ *
+ * @module /api/users/school
+ * @see {@link https://nextjs.org/docs/pages/building-your-application/routing/api-routes | Next.js API Routes Documentation}
+ */
+
+import { NextApiRequest, NextApiResponse } from "next";
+
+import { School } from "@/types/school";
+
+/**
+ * Mock data representing school entries.
+ *
+ * @constant {Partial[]}
+ */
+const mockSchools: Partial[] = [];
+for (let i = 0; i < 11; i++) {
+ mockSchools.push(
+ {
+ id: i * 3 + 1,
+ name: `School Alpha ${i + 1}`,
+ time_created: new Date(`2025-01-01T12:00:00Z`),
+ },
+ {
+ id: i * 3 + 2,
+ name: `School Beta ${i + 1}`,
+ time_created: new Date(`2025-01-01T13:00:00Z`),
+ },
+ {
+ id: i * 3 + 3,
+ name: `School Gamma ${i + 1}`,
+ time_created: new Date(`2025-01-01T14:00:00Z`),
+ },
+ );
+}
+
+/**
+ * Handles API requests to fetch schools data.
+ *
+ * @function
+ * @name handler
+ * @param {NextApiRequest} _req - The API request object. This parameter is not used in this handler.
+ * @param {NextApiResponse} res - The API response object to return the school data.
+ *
+ * @returns {void} Responds with a status code of 200 and the mock school data in JSON format.
+ */
+export default function handler(
+ _req: NextApiRequest,
+ res: NextApiResponse,
+): void {
+ res.status(200).json(mockSchools as School[]);
+}
diff --git a/client/src/pages/api/users/team.ts b/client/src/pages/api/users/team.ts
new file mode 100644
index 0000000..9c905c0
--- /dev/null
+++ b/client/src/pages/api/users/team.ts
@@ -0,0 +1,64 @@
+/**
+ * API Route Handler for fetching team data.
+ *
+ * This handler responds to API requests by returning a mock list of teams.
+ * It simulates backend behavior and serves static team data for demonstration purposes.
+ *
+ * @fileoverview API endpoint located at `/api/users/team` for fetching mock team data.
+ *
+ * @module /api/users/team
+ * @see {@link https://nextjs.org/docs/pages/building-your-application/routing/api-routes | Next.js API Routes Documentation}
+ */
+
+import { NextApiRequest, NextApiResponse } from "next";
+
+import { Team } from "@/types/team";
+
+/**
+ * Mock data representing team entries.
+ *
+ * @constant {Partial[]}
+ */
+const mockTeams: Partial[] = [];
+for (let i = 0; i < 9; i++) {
+ mockTeams.push(
+ {
+ id: i * 3 + 1,
+ name: `Team Alpha ${i + 1}`,
+ school: "Greenfield High",
+ description: `A description for Team Alpha ${i + 1}.`,
+ time_created: new Date(`2025-01-01T12:00:00Z`),
+ },
+ {
+ id: i * 3 + 2,
+ name: `Team Beta ${i + 1}`,
+ school: "Westwood Academy",
+ description: `A description for Team Beta ${i + 1}.`,
+ time_created: new Date(`2025-01-01T13:00:00Z`),
+ },
+ {
+ id: i * 3 + 3,
+ name: `Team Gamma ${i + 1}`,
+ school: "Northside School",
+ description: `A description for Team Gamma ${i + 1}.`,
+ time_created: new Date(`2025-01-01T14:00:00Z`),
+ },
+ );
+}
+
+/**
+ * Handles API requests to fetch teams data.
+ *
+ * @function
+ * @name handler
+ * @param {NextApiRequest} _req - The API request object. This parameter is not used in this handler.
+ * @param {NextApiResponse} res - The API response object to return the team data.
+ *
+ * @returns {void} Responds with a status code of 200 and the mock team data in JSON format.
+ */
+export default function handler(
+ _req: NextApiRequest,
+ res: NextApiResponse,
+): void {
+ res.status(200).json(mockTeams as Team[]);
+}
diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx
index 2b2ae1e..17dc893 100644
--- a/client/src/pages/index.tsx
+++ b/client/src/pages/index.tsx
@@ -1,10 +1,9 @@
import { useState } from "react";
-import { LoginModal } from "@/components/ui/user/login-modal";
+import { Button } from "@/components/ui/button";
+import { LoginModal } from "@/components/ui/Users/login-modal";
import { usePings } from "@/hooks/pings";
-import { Button } from "../components/ui/button";
-
export default function Home() {
const [clicked, setClicked] = useState(false);
const { data, isLoading } = usePings({
diff --git a/client/src/pages/leaderboard/index.tsx b/client/src/pages/leaderboard/index.tsx
index a1f42c1..20ad015 100644
--- a/client/src/pages/leaderboard/index.tsx
+++ b/client/src/pages/leaderboard/index.tsx
@@ -1,4 +1,4 @@
-import LeaderboardList from "@/components/ui/leaderboard/leaderboard-list";
+import LeaderboardList from "@/components/ui/Leaderboard/leaderboard-list";
import type { NextPageWithLayout } from "@/pages/_app";
/**
diff --git a/client/src/pages/users/create.tsx b/client/src/pages/users/create.tsx
new file mode 100644
index 0000000..705995c
--- /dev/null
+++ b/client/src/pages/users/create.tsx
@@ -0,0 +1,5 @@
+import DataTableForm from "@/components/ui/Users/data-table-form";
+
+export default function Create() {
+ return ;
+}
diff --git a/client/src/pages/users/create_school.tsx b/client/src/pages/users/create_school.tsx
new file mode 100644
index 0000000..e677f0f
--- /dev/null
+++ b/client/src/pages/users/create_school.tsx
@@ -0,0 +1,5 @@
+import SchoolDataTableForm from "@/components/ui/Users/school-data-table-form";
+
+export default function CreateSchool() {
+ return ;
+}
diff --git a/client/src/pages/users/create_team.tsx b/client/src/pages/users/create_team.tsx
new file mode 100644
index 0000000..7157ec6
--- /dev/null
+++ b/client/src/pages/users/create_team.tsx
@@ -0,0 +1,5 @@
+import TeamDataTableForm from "@/components/ui/Users/team-data-table-form";
+
+export default function CreateTeam() {
+ return ;
+}
diff --git a/client/src/pages/users/index.tsx b/client/src/pages/users/index.tsx
new file mode 100644
index 0000000..5f807b8
--- /dev/null
+++ b/client/src/pages/users/index.tsx
@@ -0,0 +1,74 @@
+import Link from "next/link";
+import React, { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { SearchInput } from "@/components/ui/search";
+import { DataGrid } from "@/components/ui/Users/data-grid";
+import { useFetchData } from "@/hooks/use-fetch-data";
+import type { User } from "@/types/user";
+
+export default function UserList() {
+ const {
+ data: users,
+ isLoading: isUserLoading,
+ isError: isUserError,
+ error: UserError,
+ } = useFetchData({
+ queryKey: ["user.list"],
+ endpoint: "/users",
+ });
+
+ const [page, setPage] = useState(1);
+ const [filteredData, setFilteredData] = useState([]);
+
+ useEffect(() => {
+ if (users) {
+ setFilteredData(users);
+ }
+ }, [users]);
+
+ const handleFilterChange = (value: string) => {
+ if (!users) return;
+
+ const filtered =
+ value.trim() === ""
+ ? users
+ : users.filter((item) => {
+ const query = value.toLowerCase().trim();
+ const isExactMatch = query.startsWith('"') && query.endsWith('"');
+ const normalizedQuery = isExactMatch ? query.slice(1, -1) : query;
+
+ return isExactMatch
+ ? item.username.toLowerCase() === normalizedQuery
+ : item.username.toLowerCase().includes(normalizedQuery);
+ });
+
+ setFilteredData(filtered);
+ setPage(1);
+ };
+
+ if (isUserLoading) return Loading...
;
+ if (isUserError) return Error: {UserError?.message}
;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/users/school.tsx b/client/src/pages/users/school.tsx
new file mode 100644
index 0000000..6cbc1d4
--- /dev/null
+++ b/client/src/pages/users/school.tsx
@@ -0,0 +1,74 @@
+import Link from "next/link";
+import React, { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { SearchInput } from "@/components/ui/search";
+import { SchoolDataGrid } from "@/components/ui/Users/school-data-grid";
+import { useFetchData } from "@/hooks/use-fetch-data";
+import type { School } from "@/types/school";
+
+export default function SchoolList() {
+ const {
+ data: schools,
+ isLoading: isSchoolLoading,
+ isError: isSchoolError,
+ error: schoolError,
+ } = useFetchData({
+ queryKey: ["users.school.list"],
+ endpoint: "/users/school",
+ });
+
+ const [page, setPage] = useState(1);
+ const [filteredData, setFilteredData] = useState([]);
+
+ useEffect(() => {
+ if (schools) {
+ setFilteredData(schools);
+ }
+ }, [schools]);
+
+ const handleFilterChange = (value: string) => {
+ if (!schools) return;
+
+ const filtered =
+ value.trim() === ""
+ ? schools
+ : schools.filter((item) => {
+ const query = value.toLowerCase().trim();
+ const isExactMatch = query.startsWith('"') && query.endsWith('"');
+ const normalizedQuery = isExactMatch ? query.slice(1, -1) : query;
+
+ return isExactMatch
+ ? item.name.toLowerCase() === normalizedQuery
+ : item.name.toLowerCase().includes(normalizedQuery);
+ });
+
+ setFilteredData(filtered);
+ setPage(1);
+ };
+
+ if (isSchoolLoading) return Loading...
;
+ if (isSchoolError) return Error: {schoolError?.message}
;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/pages/users/team.tsx b/client/src/pages/users/team.tsx
new file mode 100644
index 0000000..75e93f6
--- /dev/null
+++ b/client/src/pages/users/team.tsx
@@ -0,0 +1,74 @@
+import Link from "next/link";
+import React, { useEffect, useState } from "react";
+
+import { Button } from "@/components/ui/button";
+import { SearchInput } from "@/components/ui/search";
+import { TeamDataGrid } from "@/components/ui/Users/team-data-grid";
+import { useFetchData } from "@/hooks/use-fetch-data";
+import type { Team } from "@/types/team";
+
+export default function TeamList() {
+ const {
+ data: teams,
+ isLoading: isTeamLoading,
+ isError: isTeamError,
+ error: TeamError,
+ } = useFetchData({
+ queryKey: ["users.team.list"],
+ endpoint: "/users/team",
+ });
+
+ const [page, setPage] = useState(1);
+ const [filteredData, setFilteredData] = useState([]);
+
+ useEffect(() => {
+ if (teams) {
+ setFilteredData(teams);
+ }
+ }, [teams]);
+
+ const handleFilterChange = (value: string) => {
+ if (!teams) return;
+
+ const filtered =
+ value.trim() === ""
+ ? teams
+ : teams.filter((item) => {
+ const query = value.toLowerCase().trim();
+ const isExactMatch = query.startsWith('"') && query.endsWith('"');
+ const normalizedQuery = isExactMatch ? query.slice(1, -1) : query;
+
+ return isExactMatch
+ ? item.name.toLowerCase() === normalizedQuery
+ : item.name.toLowerCase().includes(normalizedQuery);
+ });
+
+ setFilteredData(filtered);
+ setPage(1);
+ };
+
+ if (isTeamLoading) return Loading...
;
+ if (isTeamError) return Error: {TeamError?.message}
;
+
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/client/src/types/app-sidebar.ts b/client/src/types/app-sidebar.ts
index 686a918..a0f18a3 100644
--- a/client/src/types/app-sidebar.ts
+++ b/client/src/types/app-sidebar.ts
@@ -56,9 +56,9 @@ export const navData: NavigationData = {
title: "User Management",
icon: UserRoundCog,
items: [
- { title: "Schools", url: "#" },
- { title: "Users", url: "#" },
- { title: "Teams", url: "#" },
+ { title: "Schools", url: "/users/school" },
+ { title: "Users", url: "/users" },
+ { title: "Teams", url: "/users/team" },
],
},
],
diff --git a/client/src/types/data-grid.ts b/client/src/types/data-grid.ts
new file mode 100644
index 0000000..0816cca
--- /dev/null
+++ b/client/src/types/data-grid.ts
@@ -0,0 +1,47 @@
+/**
+ * Represents the properties for the Datagrid component.
+ *
+ * @interface DatagridProps
+ * @property {Question[]} datacontext - The list of questions to display in the data grid.
+ * @property {(updatedData: Question[]) => void} onDataChange - Callback function triggered when the data is updated.
+ * @property {number} changePage - The current page of the data grid.
+ *
+ * @example
+ * const exampleDatagridProps: DatagridProps = {
+ * datacontext: [
+ * { name: "Question01_2024", category: "Geometry Questions", difficulty: "Difficult" },
+ * { name: "Question02_2024", category: "Algebra Questions", difficulty: "Easy" }
+ * ],
+ * onDataChange: (updatedData) => console.log("Data updated:", updatedData),
+ * changePage: 1
+ * };
+ */
+interface DatagridProps {
+ datacontext: T[];
+ onDataChange: (updatedData: T[]) => void;
+ changePage: number;
+}
+
+/**
+ * Represents the properties for the Pagination component.
+ *
+ * @interface PaginationProps
+ * @property {number} totalPages - The total number of pages available.
+ * @property {number} currentPage - The currently active page.
+ * @property {(page: number) => void} onPageChange - Callback function triggered when the page is changed.
+ * @property {string} [className] - Optional CSS class names for styling the component.
+ *
+ * @example
+ * const examplePaginationProps: PaginationProps = {
+ * totalPages: 10,
+ * currentPage: 1,
+ * onPageChange: (page) => console.log("Page changed to:", page),
+ * className: "flex text-lg"
+ * };
+ */
+interface PaginationProps {
+ totalPages: number;
+ currentPage: number;
+ onPageChange: (page: number) => void;
+ className?: string;
+}
diff --git a/client/src/types/question.ts b/client/src/types/question.ts
index bbec3d5..d2a5b2f 100644
--- a/client/src/types/question.ts
+++ b/client/src/types/question.ts
@@ -18,51 +18,3 @@ interface Question {
category: string;
difficulty: string;
}
-
-/**
- * Represents the properties for the Datagrid component.
- *
- * @interface DatagridProps
- * @property {Question[]} datacontext - The list of questions to display in the data grid.
- * @property {(updatedData: Question[]) => void} onDataChange - Callback function triggered when the data is updated.
- * @property {number} changePage - The current page of the data grid.
- *
- * @example
- * const exampleDatagridProps: DatagridProps = {
- * datacontext: [
- * { name: "Question01_2024", category: "Geometry Questions", difficulty: "Difficult" },
- * { name: "Question02_2024", category: "Algebra Questions", difficulty: "Easy" }
- * ],
- * onDataChange: (updatedData) => console.log("Data updated:", updatedData),
- * changePage: 1
- * };
- */
-interface DatagridProps {
- datacontext: Question[];
- onDataChange: (updatedData: Question[]) => void;
- changePage: number;
-}
-
-/**
- * Represents the properties for the Pagination component.
- *
- * @interface PaginationProps
- * @property {number} totalPages - The total number of pages available.
- * @property {number} currentPage - The currently active page.
- * @property {(page: number) => void} onPageChange - Callback function triggered when the page is changed.
- * @property {string} [className] - Optional CSS class names for styling the component.
- *
- * @example
- * const examplePaginationProps: PaginationProps = {
- * totalPages: 10,
- * currentPage: 1,
- * onPageChange: (page) => console.log("Page changed to:", page),
- * className: "flex text-lg"
- * };
- */
-interface PaginationProps {
- totalPages: number;
- currentPage: number;
- onPageChange: (page: number) => void;
- className?: string;
-}
diff --git a/client/src/types/school.ts b/client/src/types/school.ts
new file mode 100644
index 0000000..0e5e1ca
--- /dev/null
+++ b/client/src/types/school.ts
@@ -0,0 +1,11 @@
+import { z } from "zod";
+
+export interface School {
+ id: number;
+ name: string;
+ time_created: Date;
+}
+
+export const createSchoolSchema = z.object({
+ name: z.string().min(1, "Required"),
+});
diff --git a/client/src/types/team.ts b/client/src/types/team.ts
new file mode 100644
index 0000000..1c817f7
--- /dev/null
+++ b/client/src/types/team.ts
@@ -0,0 +1,15 @@
+import { z } from "zod";
+
+export interface Team {
+ id: number;
+ name: string;
+ school: string;
+ description: string;
+ time_created: Date;
+}
+
+export const createTeamSchema = z.object({
+ name: z.string().min(1, "Required"),
+ school_id: z.number({ message: "Required" }),
+ description: z.string().min(1, "Required"),
+});
diff --git a/client/src/types/user.ts b/client/src/types/user.ts
index f1e947f..8320833 100644
--- a/client/src/types/user.ts
+++ b/client/src/types/user.ts
@@ -6,7 +6,10 @@ import { z } from "zod";
* @example
* const role: Role = "student";
*/
-export type Role = "admin" | "teacher" | "student";
+export const RoleEnum = z.enum(["admin", "teacher", "student"], {
+ errorMap: () => ({ message: "Invalid User Role" }),
+});
+export type Role = z.infer;
/**
* Represents a user object.
@@ -17,6 +20,7 @@ export type Role = "admin" | "teacher" | "student";
* @property {string} first_name - The first name of the user.
* @property {string} last_name - The last name of the user.
* @property {Role} role - The role of the user (admin, teacher, or student).
+ * @property {string} school - The school name attached to the user.
*/
export interface User {
id: number;
@@ -25,6 +29,7 @@ export interface User {
first_name: string;
last_name: string;
role: Role;
+ school: string;
}
/**
@@ -44,3 +49,9 @@ export const loginSchema = z.object({
.regex(/[0-9]/, "Password must contain numbers")
.regex(/[^A-Za-z0-9]/, "Password must contain symbols"),
});
+
+export const createUserSchema = loginSchema.extend({
+ userRole: RoleEnum,
+ school_id: z.number({ message: "Required" }),
+ email: z.string().email("Invalid email address").optional(),
+});
From a87bffdf7960165ffb32e0e39e832f4763d59e56 Mon Sep 17 00:00:00 2001
From: Lok Yx
Date: Wed, 15 Jan 2025 08:28:34 +0000
Subject: [PATCH 2/3] feat(admin): update sidebar, refactor, jsdoc User
Management module
- feat(sidebar): add "Admin Portal" link to sidebar for navigation to Django Admin Portal
- refactor(admin): update function exports to use named and default exports
- docs(admin): add JSDoc comments to User Management module related files
---
client/.env.example | 1 +
.../ui/Leaderboard/leaderboard-list.tsx | 8 ++---
client/src/components/ui/Users/data-grid.tsx | 31 +++++++++++++++++++
.../components/ui/Users/data-table-form.tsx | 24 +++++++++++++-
.../src/components/ui/Users/login-modal.tsx | 2 --
.../components/ui/Users/school-data-grid.tsx | 10 ++++++
.../ui/Users/school-data-table-form.tsx | 13 +++++++-
.../src/components/ui/Users/select-school.tsx | 26 ++++++++++++++++
.../components/ui/Users/team-data-grid.tsx | 11 +++++++
.../ui/Users/team-data-table-form.tsx | 13 +++++++-
client/src/components/ui/app-sidebar.tsx | 6 +++-
client/src/components/ui/select-role.tsx | 29 +++++++++++++++++
client/src/lib/api.ts | 2 ++
client/src/pages/leaderboard/index.tsx | 2 +-
client/src/pages/users/create.tsx | 2 +-
client/src/pages/users/create_school.tsx | 2 +-
client/src/pages/users/create_team.tsx | 2 +-
client/src/types/app-sidebar.ts | 4 +++
client/src/types/school.ts | 15 +++++++++
client/src/types/team.ts | 18 +++++++++++
client/src/types/user.ts | 31 +++++++++++++++++++
21 files changed, 236 insertions(+), 16 deletions(-)
diff --git a/client/.env.example b/client/.env.example
index 63e7f21..0413e6e 100644
--- a/client/.env.example
+++ b/client/.env.example
@@ -1,4 +1,5 @@
REACT_EDITOR=code
APP_ENV=DEVELOPMENT
+NEXT_PUBLIC_BACKEND_URL_BASE="http://localhost:8000/"
NEXT_PUBLIC_BACKEND_URL="http://localhost:8000/api"
\ No newline at end of file
diff --git a/client/src/components/ui/Leaderboard/leaderboard-list.tsx b/client/src/components/ui/Leaderboard/leaderboard-list.tsx
index 0412854..e2c4c96 100644
--- a/client/src/components/ui/Leaderboard/leaderboard-list.tsx
+++ b/client/src/components/ui/Leaderboard/leaderboard-list.tsx
@@ -17,10 +17,8 @@ import type { Leaderboard } from "@/types/leaderboard";
* It fetches leaderboard data from an API and allows users to filter the list based on
* leaderboard name and status. The component also handles loading and error states for
* data fetching.
- *
- * @returns {JSX.Element} The rendered LeaderboardList component.
*/
-const LeaderboardList = () => {
+export function LeaderboardList() {
const [filters, setFilters] = useState({
search: "",
status: "All",
@@ -165,6 +163,4 @@ const LeaderboardList = () => {
);
-};
-
-export default LeaderboardList;
+}
diff --git a/client/src/components/ui/Users/data-grid.tsx b/client/src/components/ui/Users/data-grid.tsx
index cf427b9..a432138 100644
--- a/client/src/components/ui/Users/data-grid.tsx
+++ b/client/src/components/ui/Users/data-grid.tsx
@@ -13,6 +13,37 @@ import {
import { cn } from "@/lib/utils";
import { User } from "@/types/user";
+/**
+ * Renders a paginated data grid for displaying user information.
+ *
+ * The `DataGrid` component provides a table-based UI for displaying user data
+ * with support for pagination. It handles data slicing, empty cell padding,
+ * and provides callback handlers for changes in data and pagination.
+ *
+ * @function DataGrid
+ * @template T - The type of data being displayed in the data grid.
+ * @param {Object} props - The props object.
+ * @param {User[]} props.datacontext - The array of data items to be displayed in the grid.
+ * @param {function(User[]): void} props.onDataChange - Callback triggered when the data changes.
+ * @param {number} props.changePage - The page number to navigate to when the data changes.
+ *
+ * @example
+ * // Example usage
+ * const users = [
+ * { id: 1, username: "admin", role: "admin", school: "Greenfield High" },
+ * { id: 2, username: "teacher1", role: "teacher", school: "Westwood Academy" },
+ * ];
+ *
+ * const handleDataChange = (updatedData) => {
+ * console.log(updatedData);
+ * };
+ *
+ * ;
+ */
export function DataGrid({
datacontext,
onDataChange,
diff --git a/client/src/components/ui/Users/data-table-form.tsx b/client/src/components/ui/Users/data-table-form.tsx
index 8dcebdc..83283eb 100644
--- a/client/src/components/ui/Users/data-table-form.tsx
+++ b/client/src/components/ui/Users/data-table-form.tsx
@@ -27,7 +27,29 @@ import { createUserSchema } from "@/types/user";
type User = z.infer;
-export default function DataTableForm() {
+/**
+ * Renders a data table form for managing user information with a dynamic table structure.
+ *
+ * The `DataTableForm` component allows users to add, and delete rows of user data.
+ * It uses `react-hook-form` with Zod schema validation to manage and validate the form state.
+ * Each row includes fields for `Username`, `Password`, `Email`, `User Role`, and `School`.
+ *
+ * @function DataTableForm
+ *
+ * @description
+ * The component utilizes the following libraries and components:
+ * - `react-hook-form` for form state management.
+ * - `zod` for schema validation.
+ *
+ * Features:
+ * - Dynamically adds or removes rows with user data.
+ * - Provides validation for all input fields.
+ * - Submits the collected data as an array of users.
+ *
+ * Additional Reference:
+ * - {@link https://react-hook-form.com/docs/usefieldarray React Hook Form: useFieldArray}
+ */
+export function DataTableForm() {
const defaultUser = {
username: "",
password: "",
diff --git a/client/src/components/ui/Users/login-modal.tsx b/client/src/components/ui/Users/login-modal.tsx
index 1645c64..164581b 100644
--- a/client/src/components/ui/Users/login-modal.tsx
+++ b/client/src/components/ui/Users/login-modal.tsx
@@ -182,5 +182,3 @@ export function LoginModal({ children }: LoginFormProps) {
);
}
-
-export default LoginModal;
diff --git a/client/src/components/ui/Users/school-data-grid.tsx b/client/src/components/ui/Users/school-data-grid.tsx
index 55570fa..44cb2a8 100644
--- a/client/src/components/ui/Users/school-data-grid.tsx
+++ b/client/src/components/ui/Users/school-data-grid.tsx
@@ -13,6 +13,16 @@ import {
import { cn } from "@/lib/utils";
import { School } from "@/types/school";
+/**
+ * Renders a paginated data grid for displaying school information.
+ *
+ * The `SchoolDataGrid` component provides a table-based UI for displaying school data
+ * with support for pagination. The behavior is similar to the `UserDataGrid`, but
+ * it is tailored to display school-specific fields such as `School Id`, `School Name`,
+ * and `Created On`.
+ *
+ * @see [UserDataGrid](./data-grid.tsx) for reference.
+ */
export function SchoolDataGrid({
datacontext,
onDataChange,
diff --git a/client/src/components/ui/Users/school-data-table-form.tsx b/client/src/components/ui/Users/school-data-table-form.tsx
index c9dff9d..f9709be 100644
--- a/client/src/components/ui/Users/school-data-table-form.tsx
+++ b/client/src/components/ui/Users/school-data-table-form.tsx
@@ -24,7 +24,18 @@ import { createSchoolSchema } from "@/types/school";
type School = z.infer;
-export default function SchoolDataTableForm() {
+/**
+ * Renders a data table form for managing school information with dynamic rows.
+ *
+ * @function SchoolDataTableForm
+ *
+ * @description
+ * This component provides a table-based form for managing schools. Users can add, remove, and edit school entries dynamically.
+ *
+ * Similar Implementation:
+ * @see [UserDataTableForm](./data-table-form.tsx) for reference.
+ */
+export function SchoolDataTableForm() {
const defaultSchool = {
name: "",
} as School;
diff --git a/client/src/components/ui/Users/select-school.tsx b/client/src/components/ui/Users/select-school.tsx
index dca4286..827fae8 100644
--- a/client/src/components/ui/Users/select-school.tsx
+++ b/client/src/components/ui/Users/select-school.tsx
@@ -15,6 +15,32 @@ type Props = {
className?: string;
};
+/**
+ * A form field for selecting a school from a dropdown list fetched from an API.
+ *
+ * @param {Object} props - The props for the component.
+ * @param {number | undefined} props.selectedId - The currently selected school ID.
+ * @param {Function} props.onChange - A callback function that handles the change in school ID.
+ * @param {string} [props.className] - An optional className to customize the component's styling.
+ *
+ * @example
+ * (
+ *
+ *
+ *
+ *
+ *
+ *
+ * )}
+ * />
+ */
export function SelectSchool({ selectedId, onChange, className }: Props) {
const {
data: schools,
diff --git a/client/src/components/ui/Users/team-data-grid.tsx b/client/src/components/ui/Users/team-data-grid.tsx
index d32be2c..e65b65d 100644
--- a/client/src/components/ui/Users/team-data-grid.tsx
+++ b/client/src/components/ui/Users/team-data-grid.tsx
@@ -13,6 +13,17 @@ import {
import { cn } from "@/lib/utils";
import { Team } from "@/types/team";
+/**
+ * Renders a paginated data grid for displaying team information.
+ *
+ * The `TeamDataGrid` component provides a table-based UI for displaying team data
+ * with support for pagination. The behavior is similar to the `UserDataGrid`, but
+ * it is tailored to display team-specific fields such as `Team Id`, `Team Name`,
+ * `School`, `Description`, and `Created On`.
+ *
+ * Similar Implementation:
+ * @see [UserDataGrid](./data-grid.tsx) for reference.
+ */
export function TeamDataGrid({
datacontext,
onDataChange,
diff --git a/client/src/components/ui/Users/team-data-table-form.tsx b/client/src/components/ui/Users/team-data-table-form.tsx
index 122e936..34bb898 100644
--- a/client/src/components/ui/Users/team-data-table-form.tsx
+++ b/client/src/components/ui/Users/team-data-table-form.tsx
@@ -26,7 +26,18 @@ import { createTeamSchema } from "@/types/team";
type Team = z.infer;
-export default function TeamDataTableForm() {
+/**
+ * Renders a data table form for managing team information with dynamic rows.
+ *
+ * @function TeamDataTableForm
+ *
+ * @description
+ * This component provides a table-based form for managing teams. It allows users to dynamically add, remove, and edit team entries.
+ *
+ * Similar Implementation:
+ * @see [UserDataTableForm](./data-table-form.tsx) for reference.
+ */
+export function TeamDataTableForm() {
const defaultTeam = {
name: "",
description: "",
diff --git a/client/src/components/ui/app-sidebar.tsx b/client/src/components/ui/app-sidebar.tsx
index 8b403d9..a3d8cf8 100644
--- a/client/src/components/ui/app-sidebar.tsx
+++ b/client/src/components/ui/app-sidebar.tsx
@@ -99,7 +99,11 @@ export function AppSidebar({ Role, ...props }: AppSidebarProps) {
isActive={item.isActive}
className="hover:bg-yellow data-[active=true]:bg-yellow"
>
-
+
{item.title}
diff --git a/client/src/components/ui/select-role.tsx b/client/src/components/ui/select-role.tsx
index 9da3a2e..6cf9d19 100644
--- a/client/src/components/ui/select-role.tsx
+++ b/client/src/components/ui/select-role.tsx
@@ -14,6 +14,35 @@ type Props = {
className?: string;
};
+/**
+ * Renders a dropdown for selecting a user role.
+ *
+ * @description
+ * This component provides a customizable dropdown for selecting a role. It is designed to work with the `RoleEnum` enum, dynamically rendering role options.
+ *
+ * @param {Object} props - The component props.
+ * @param {string | undefined} props.selectedRole - The currently selected role.
+ * @param {Function} props.onChange - Callback function triggered when the selected role changes.
+ * @param {string} [props.className] - Optional additional class names for the select trigger.
+ *
+ * @example
+ * (
+ *
+ *
+ *
+ *
+ *
+ *
+ * )}
+ * />
+ */
export function SelectRole({ selectedRole, onChange, className }: Props) {
const onValueChange = (value: string) => {
onChange(value);
diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts
index b2b19da..7cddc60 100644
--- a/client/src/lib/api.ts
+++ b/client/src/lib/api.ts
@@ -1,5 +1,7 @@
import axios from "axios";
+export const backendURL = process.env.NEXT_PUBLIC_BACKEND_URL_BASE;
+
const baseURL =
process.env.NODE_ENV === "development"
? "http://localhost:3000/api" // temporarily use port 3000 in dev for mock data
diff --git a/client/src/pages/leaderboard/index.tsx b/client/src/pages/leaderboard/index.tsx
index 20ad015..202cd5b 100644
--- a/client/src/pages/leaderboard/index.tsx
+++ b/client/src/pages/leaderboard/index.tsx
@@ -1,4 +1,4 @@
-import LeaderboardList from "@/components/ui/Leaderboard/leaderboard-list";
+import { LeaderboardList } from "@/components/ui/Leaderboard/leaderboard-list";
import type { NextPageWithLayout } from "@/pages/_app";
/**
diff --git a/client/src/pages/users/create.tsx b/client/src/pages/users/create.tsx
index 705995c..97bd631 100644
--- a/client/src/pages/users/create.tsx
+++ b/client/src/pages/users/create.tsx
@@ -1,4 +1,4 @@
-import DataTableForm from "@/components/ui/Users/data-table-form";
+import { DataTableForm } from "@/components/ui/Users/data-table-form";
export default function Create() {
return ;
diff --git a/client/src/pages/users/create_school.tsx b/client/src/pages/users/create_school.tsx
index e677f0f..490bf51 100644
--- a/client/src/pages/users/create_school.tsx
+++ b/client/src/pages/users/create_school.tsx
@@ -1,4 +1,4 @@
-import SchoolDataTableForm from "@/components/ui/Users/school-data-table-form";
+import { SchoolDataTableForm } from "@/components/ui/Users/school-data-table-form";
export default function CreateSchool() {
return ;
diff --git a/client/src/pages/users/create_team.tsx b/client/src/pages/users/create_team.tsx
index 7157ec6..8170741 100644
--- a/client/src/pages/users/create_team.tsx
+++ b/client/src/pages/users/create_team.tsx
@@ -1,4 +1,4 @@
-import TeamDataTableForm from "@/components/ui/Users/team-data-table-form";
+import { TeamDataTableForm } from "@/components/ui/Users/team-data-table-form";
export default function CreateTeam() {
return ;
diff --git a/client/src/types/app-sidebar.ts b/client/src/types/app-sidebar.ts
index a0f18a3..58687a1 100644
--- a/client/src/types/app-sidebar.ts
+++ b/client/src/types/app-sidebar.ts
@@ -6,10 +6,13 @@ import {
UserRoundCog,
} from "lucide-react";
+import { backendURL } from "@/lib/api";
+
interface MenuItem {
title: string;
url: string; // need to be unique
isActive?: boolean;
+ isNewTab?: boolean;
}
interface MenuSection {
@@ -59,6 +62,7 @@ export const navData: NavigationData = {
{ title: "Schools", url: "/users/school" },
{ title: "Users", url: "/users" },
{ title: "Teams", url: "/users/team" },
+ { title: "Admin Portal", url: `${backendURL}admin`, isNewTab: true },
],
},
],
diff --git a/client/src/types/school.ts b/client/src/types/school.ts
index 0e5e1ca..64c42b3 100644
--- a/client/src/types/school.ts
+++ b/client/src/types/school.ts
@@ -1,11 +1,26 @@
import { z } from "zod";
+/**
+ * Represents a school with its id, name, and creation date.
+ *
+ * @interface School
+ * @property {number} id - The unique identifier for the school.
+ * @property {string} name - The name of the school.
+ * @property {Date} time_created - The timestamp of when the school was created.
+ */
export interface School {
id: number;
name: string;
time_created: Date;
}
+/**
+ * A Zod schema for validating the creation of a school.
+ *
+ * @example
+ * const schoolData = { name: "University of Example" };
+ * const parsedData = createSchoolSchema.parse(schoolData);
+ */
export const createSchoolSchema = z.object({
name: z.string().min(1, "Required"),
});
diff --git a/client/src/types/team.ts b/client/src/types/team.ts
index 1c817f7..6253424 100644
--- a/client/src/types/team.ts
+++ b/client/src/types/team.ts
@@ -1,5 +1,15 @@
import { z } from "zod";
+/**
+ * Represents a team with its id, name, associated school, description, and creation date.
+ *
+ * @interface Team
+ * @property {number} id - The unique identifier for the team.
+ * @property {string} name - The name of the team.
+ * @property {string} school - The name of the school associated with the team.
+ * @property {string} description - A description of the team.
+ * @property {Date} time_created - The timestamp of when the team was created.
+ */
export interface Team {
id: number;
name: string;
@@ -8,6 +18,14 @@ export interface Team {
time_created: Date;
}
+/**
+ * A Zod schema for validating the creation of a team.
+ *
+ * @example
+ * // Example usage of createTeamSchema to validate team data
+ * const teamData = { name: "Team A", school_id: 1, description: "A great team" };
+ * const parsedData = createTeamSchema.parse(teamData);
+ */
export const createTeamSchema = z.object({
name: z.string().min(1, "Required"),
school_id: z.number({ message: "Required" }),
diff --git a/client/src/types/user.ts b/client/src/types/user.ts
index 8320833..665c81b 100644
--- a/client/src/types/user.ts
+++ b/client/src/types/user.ts
@@ -9,11 +9,21 @@ import { z } from "zod";
export const RoleEnum = z.enum(["admin", "teacher", "student"], {
errorMap: () => ({ message: "Invalid User Role" }),
});
+
+/**
+ * Type representing a user role. Can be one of the following:
+ * - "admin"
+ * - "teacher"
+ * - "student"
+ *
+ * @type {Role}
+ */
export type Role = z.infer;
/**
* Represents a user object.
*
+ * @interface User
* @property {number} id - The unique identifier of the user.
* @property {string} username - The username of the user.
* @property {string} email - The email of the user.
@@ -35,6 +45,13 @@ export interface User {
/**
* Zod schema for validating login input.
*
+ * @example
+ * // Example usage:
+ * const validData = loginSchema.parse({
+ * username: "admin123",
+ * password: "admin123#",
+ * });
+ *
* @const {z.ZodSchema} loginSchema
*
* @property {z.ZodString} username - Required, must be at least 1 character.
@@ -50,6 +67,20 @@ export const loginSchema = z.object({
.regex(/[^A-Za-z0-9]/, "Password must contain symbols"),
});
+/**
+ * Zod schema for validating user creation input.
+ *
+ * Extends the login schema to include additional user-related fields.
+ *
+ * @example
+ * const validUser = createUserSchema.parse({
+ * username: "admin123",
+ * password: "admin123#",
+ * userRole: "student",
+ * school_id: 1,
+ * email: "user@example.com",
+ * });
+ */
export const createUserSchema = loginSchema.extend({
userRole: RoleEnum,
school_id: z.number({ message: "Required" }),
From e3b0c5a099b017306536476b4ace9a22f7cc2884 Mon Sep 17 00:00:00 2001
From: Lok Yx
Date: Fri, 17 Jan 2025 12:05:45 +0000
Subject: [PATCH 3/3] feat(ui): improve layout, sidebar, loading state
- feat(ui): create `WaitingLoader` component to show loading state during requests
- feat(layout): get layout based on login status and role. Add breadcrumb navigation links
- feat(sidebar): add logo to navigate landing page, add logout button
- fix(sidebar): add missing `SheetTitle` and `SheetDescription` in Sidebar
- feat(api): add `/users/me` mock data API
- style(ui): add no-scrollbar CSS for better layout appearance
---
client/src/components/layout.tsx | 56 ++++++++++--
client/src/components/sidebar-layout.tsx | 32 -------
client/src/components/sidebar.tsx | 78 ++++++++++++++++
.../ui/Leaderboard/leaderboard-list.tsx | 3 +-
client/src/components/ui/app-sidebar.tsx | 89 ++++++++++++++-----
client/src/components/ui/loading.tsx | 22 +++++
client/src/components/ui/sidebar.tsx | 19 +++-
client/src/pages/_app.tsx | 11 ++-
client/src/pages/api/users/me.ts | 56 ++++++++++++
client/src/pages/index.tsx | 11 +--
client/src/pages/question/index.tsx | 16 +---
client/src/pages/users/index.tsx | 3 +-
client/src/pages/users/school.tsx | 3 +-
client/src/pages/users/team.tsx | 3 +-
client/src/styles/globals.css | 9 ++
15 files changed, 322 insertions(+), 89 deletions(-)
delete mode 100644 client/src/components/sidebar-layout.tsx
create mode 100644 client/src/components/sidebar.tsx
create mode 100644 client/src/components/ui/loading.tsx
create mode 100644 client/src/pages/api/users/me.ts
diff --git a/client/src/components/layout.tsx b/client/src/components/layout.tsx
index 5d2bc57..fe85e83 100644
--- a/client/src/components/layout.tsx
+++ b/client/src/components/layout.tsx
@@ -1,16 +1,58 @@
-import React from "react";
+import React, { useEffect, useState } from "react";
import Navbar from "@/components/navbar";
+import { WaitingLoader } from "@/components/ui/loading";
+import { useAuth } from "@/context/auth-provider";
+import { useFetchData } from "@/hooks/use-fetch-data";
+import { User } from "@/types/user";
+
+import Sidebar from "./sidebar";
interface LayoutProps {
children: React.ReactNode;
}
+/**
+ * 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 }: LayoutProps) {
- return (
-
-
- {children}
-
- );
+ const [isAuthChecked, setIsAuthChecked] = useState(false);
+ const { userId } = useAuth();
+ const isLoggedIn = Boolean(userId);
+
+ // wait for auth to be checked before rendering
+ useEffect(() => {
+ setIsAuthChecked(true);
+ }, []);
+
+ const {
+ data: user,
+ error,
+ isLoading,
+ } = useFetchData({
+ queryKey: ["user", userId],
+ endpoint: "/users/me",
+ staleTime: 5 * 60 * 1000,
+ enabled: Boolean(userId),
+ });
+
+ if (!isAuthChecked) return null;
+
+ if (!isLoggedIn) {
+ return (
+
+
+ {children}
+
+ );
+ }
+
+ if (isLoading) return ;
+ if (!user) return {error?.message || "Failed to load user data."}
;
+
+ return {children};
}
diff --git a/client/src/components/sidebar-layout.tsx b/client/src/components/sidebar-layout.tsx
deleted file mode 100644
index 5e01456..0000000
--- a/client/src/components/sidebar-layout.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import React from "react";
-
-import AppSidebar from "@/components/ui/app-sidebar";
-import { Separator } from "@/components/ui/separator";
-import {
- SidebarInset,
- SidebarProvider,
- SidebarTrigger,
-} from "@/components/ui/sidebar";
-import { Role } from "@/types/user";
-
-interface LayoutProps {
- children: React.ReactNode;
- role: Role;
-}
-
-export default function SidebarLayout({ children, role }: LayoutProps) {
- return (
-
-
-
-
-
- {children}
-
-
-
- );
-}
diff --git a/client/src/components/sidebar.tsx b/client/src/components/sidebar.tsx
new file mode 100644
index 0000000..00321dd
--- /dev/null
+++ b/client/src/components/sidebar.tsx
@@ -0,0 +1,78 @@
+import { useRouter } from "next/router";
+import React from "react";
+
+import AppSidebar from "@/components/ui/app-sidebar";
+import { Separator } from "@/components/ui/separator";
+import {
+ SidebarInset,
+ SidebarProvider,
+ SidebarTrigger,
+} 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;
+}
+
+/**
+ * Sidebar layout component that renders a sidebar with breadcrumb navigation.
+ * Displays different content based on the user's role.
+ *
+ * @param {LayoutProps} props - The component props.
+ * @param {React.ReactNode} props.children - The content to render inside the sidebar layout.
+ * @param {Role} props.role - The role of the user, used to control sidebar behavior.
+ */
+export default function Sidebar({ children, role }: LayoutProps) {
+ const router = useRouter();
+ const pathSegments = router.pathname.split("/").filter(Boolean);
+
+ const breadcrumbItems = pathSegments.map((segment, index) => {
+ const formattedSegment = segment
+ .replace(/_/g, " ") // replace underscores with spaces
+ .replace(/\b\w/g, (char) => char.toUpperCase()); // capitalize first letter
+ return {
+ title: formattedSegment,
+ url: `/${pathSegments.slice(0, index + 1).join("/")}`,
+ };
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
+ {breadcrumbItems.map((item, index) => (
+
+
+
+ {item.title}
+
+
+ {index < breadcrumbItems.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/client/src/components/ui/Leaderboard/leaderboard-list.tsx b/client/src/components/ui/Leaderboard/leaderboard-list.tsx
index e2c4c96..2d15ec6 100644
--- a/client/src/components/ui/Leaderboard/leaderboard-list.tsx
+++ b/client/src/components/ui/Leaderboard/leaderboard-list.tsx
@@ -1,5 +1,6 @@
import { useState } from "react";
+import { WaitingLoader } from "@/components/ui/loading";
import { Search, SearchInput, SearchSelect } from "@/components/ui/search";
import {
Table,
@@ -34,7 +35,7 @@ export function LeaderboardList() {
endpoint: "leaderboard/list",
});
- if (isLeaderboardLoading) return Loading...
;
+ if (isLeaderboardLoading) return ;
if (isLeaderboardError) {
console.error("Error fetching leaderboards:", leaderboardError);
diff --git a/client/src/components/ui/app-sidebar.tsx b/client/src/components/ui/app-sidebar.tsx
index c5c8bb0..2b43e6e 100644
--- a/client/src/components/ui/app-sidebar.tsx
+++ b/client/src/components/ui/app-sidebar.tsx
@@ -1,6 +1,8 @@
-import { ChevronRight } from "lucide-react";
+import { ChevronRight, LogOut } from "lucide-react";
+import Image from "next/image";
+import Link from "next/link";
import { useRouter } from "next/router";
-import React from "react";
+import React, { useMemo, useState } from "react";
import {
Collapsible,
@@ -19,14 +21,14 @@ import {
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
- SidebarRail,
} from "@/components/ui/sidebar";
+import { useAuth } from "@/context/auth-provider";
import { cn } from "@/lib/utils";
import { navData } from "@/types/app-sidebar";
import { Role } from "@/types/user";
interface AppSidebarProps extends React.ComponentProps {
- Role: Role;
+ Role: Role | null;
}
/**
@@ -48,29 +50,64 @@ interface AppSidebarProps extends React.ComponentProps {
* @param {Role} props.Role - The role of the user (e.g., Admin, Staff, Student), used to determine the navigation items to display.
*/
export default function AppSidebar({ Role, ...props }: AppSidebarProps) {
+ const { logout } = useAuth();
const router = useRouter();
- const roleNavData = navData[Role];
- roleNavData.forEach((section) => {
- for (const item of section.items) {
- const regex = new RegExp(`^${item.url.replace(/\[.*?\]/g, ".*")}$`);
- item.isActive = regex.test(router.pathname);
- if (item.isActive && !section.isActive) {
- section.isActive = true;
- }
- }
- });
+ const [openSection, setOpenSection] = useState(null);
+
+ // Memoize role-based navigation data to avoid recalculating on every render.
+ const roleNavData = useMemo(() => {
+ if (!Role) return [];
+
+ const isPathActive = (path: string) => {
+ const regex = new RegExp(`^${path.replace(/\[.*?\]/g, ".*")}$`);
+ return regex.test(router.pathname);
+ };
+
+ return navData[Role].map((section) => ({
+ ...section,
+ isActive: section.items.some((item) => isPathActive(item.url)),
+ items: section.items.map((item) => ({
+ ...item,
+ isActive: isPathActive(item.url),
+ })),
+ }));
+ }, [Role, router.pathname]);
+
+ const handleMenuToggle = (sectionTitle: string | null) => {
+ setOpenSection((prev) => (prev === sectionTitle ? null : sectionTitle));
+ };
+
+ const handleLogout = () => {
+ router.push("/");
+ logout();
+ };
return (
-
+
+
+
+
+
+
{roleNavData.map((section) => (
-
+
+ handleMenuToggle(isOpen ? section.title : null)
+ }
className="group/collapsible"
>
@@ -98,14 +135,15 @@ export default function AppSidebar({ Role, ...props }: AppSidebarProps) {
asChild
isActive={item.isActive}
className="hover:bg-yellow data-[active=true]:bg-yellow"
+ onClick={() => handleMenuToggle(section.title)}
>
-
{item.title}
-
+
))}
@@ -117,10 +155,17 @@ export default function AppSidebar({ Role, ...props }: AppSidebarProps) {
))}
-
+
+
+
+
+ Logout
+
+
+
Ctrl/Cmd + b to hide me
-
+ {/* */}
);
}
diff --git a/client/src/components/ui/loading.tsx b/client/src/components/ui/loading.tsx
new file mode 100644
index 0000000..63e624b
--- /dev/null
+++ b/client/src/components/ui/loading.tsx
@@ -0,0 +1,22 @@
+import { Loader } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+
+/**
+ * WaitingLoader component displays a loading spinner inside a disabled button, indicating that content is being loaded.
+ *
+ * **Usage:**
+ * Simply include where you need to display a loading state within the UI.
+ *
+ * @example
+ * if (isLoading) return ;
+ */
+export function WaitingLoader() {
+ return (
+
+
+
+ );
+}
diff --git a/client/src/components/ui/sidebar.tsx b/client/src/components/ui/sidebar.tsx
index 782a4c0..af71e81 100644
--- a/client/src/components/ui/sidebar.tsx
+++ b/client/src/components/ui/sidebar.tsx
@@ -6,7 +6,12 @@ import React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
-import { Sheet, SheetContent } from "@/components/ui/sheet";
+import {
+ Sheet,
+ SheetContent,
+ SheetDescription,
+ SheetTitle,
+} from "@/components/ui/sheet";
import { Skeleton } from "@/components/ui/skeleton";
import {
Tooltip,
@@ -223,6 +228,18 @@ const Sidebar = React.forwardRef<
side={side}
aria-label="Sidebar Sheet Content"
>
+ {/*
+ Added SheetTitle, SheetDescription for accessibility to avoid console errors.
+ 'sr-only' class only visible to screen readers, not visually to the user.
+ References:
+ https://stackoverflow.com/questions/78728076/shadcn-dialogcontent-requires-a-dialogtitle-for-the-component-to-be-accessib
+ https://stackoverflow.com/questions/78728117/shadcn-warning-missing-description-or-aria-describedby-undefined-for-dia
+ https://tailwindcss.com/docs/screen-readers
+ */}
+ Menu
+
+ Description goes here
+
{children}
diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx
index 2b0bd56..3a8e5d4 100644
--- a/client/src/pages/_app.tsx
+++ b/client/src/pages/_app.tsx
@@ -7,6 +7,7 @@ import type { AppProps } from "next/app";
import { Roboto, Urbanist } from "next/font/google";
import type { ReactElement, ReactNode } from "react";
+import Layout from "@/components/layout";
import { Toaster } from "@/components/ui/sonner";
import { AuthProvider } from "@/context/auth-provider";
@@ -37,7 +38,13 @@ type AppPropsWithLayout = AppProps & {
const queryClient = new QueryClient();
export default function App({ Component, pageProps }: AppPropsWithLayout) {
- const getLayout = Component.getLayout ?? ((page) => page);
+ const myLayout = Component.getLayout ? (
+ Component.getLayout()
+ ) : (
+
+
+
+ );
return (
<>
@@ -50,7 +57,7 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
- {getLayout()}
+ {myLayout}
diff --git a/client/src/pages/api/users/me.ts b/client/src/pages/api/users/me.ts
new file mode 100644
index 0000000..2aa422f
--- /dev/null
+++ b/client/src/pages/api/users/me.ts
@@ -0,0 +1,56 @@
+/**
+ * API Route Handler for fetching user data.
+ *
+ * This handler serves mock user data for demonstration purposes.
+ * It is accessible at the `/api/users/me` endpoint.
+ *
+ * @fileoverview Provides an API route for retrieving mock user data.
+ * @module api/users/me
+ */
+
+import { NextApiRequest, NextApiResponse } from "next";
+
+import { User } from "@/types/user";
+
+/**
+ * Mock user data for demonstration purposes.
+ *
+ * @constant {User}
+ */
+const mockUser: User = {
+ id: 1,
+ username: "johndoe",
+ email: "johndoe@example.com",
+ first_name: "John",
+ last_name: "Doe",
+ role: "admin",
+ school: "University of Western Australia",
+};
+
+/**
+ * API route handler to fetch user data.
+ *
+ * @function handler
+ * @param {NextApiRequest} _req - The incoming API request object. Ignored in this mock implementation.
+ * @param {NextApiResponse} res - The outgoing API response object containing the mock user data.
+ *
+ * @returns {void} Responds with a status code of 200 and the mock user data in JSON format.
+ *
+ * @example
+ * // Example response payload:
+ * // {
+ * // "id": 1,
+ * // "username": "johndoe",
+ * // "email": "johndoe@example.com",
+ * // "first_name": "John",
+ * // "last_name": "Doe",
+ * // "role": "admin",
+ * // "school": "University of Western Australia"
+ * // }
+ */
+export default function handler(
+ _req: NextApiRequest,
+ res: NextApiResponse,
+): void {
+ res.status(200).json(mockUser);
+}
diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx
index 7adb90e..2660452 100644
--- a/client/src/pages/index.tsx
+++ b/client/src/pages/index.tsx
@@ -2,10 +2,9 @@ import { useState } from "react";
import { usePings } from "@/hooks/pings";
-import Layout from "../components/layout";
import { Button } from "../components/ui/button";
-const Home = () => {
+export default function Home() {
const [clicked, setClicked] = useState(false);
const { data, isLoading } = usePings({
enabled: clicked,
@@ -27,10 +26,4 @@ const Home = () => {
);
-};
-
-Home.getLayout = function getLayout(page: React.ReactElement) {
- return {page};
-};
-
-export default Home;
+}
diff --git a/client/src/pages/question/index.tsx b/client/src/pages/question/index.tsx
index c31c6d6..6d5d303 100644
--- a/client/src/pages/question/index.tsx
+++ b/client/src/pages/question/index.tsx
@@ -1,13 +1,13 @@
import Link from "next/link";
import React, { useEffect, useState } from "react";
-import SidebarLayout from "@/components/sidebar-layout";
import { Button } from "@/components/ui/button";
+import { WaitingLoader } from "@/components/ui/loading";
import { Datagrid } from "@/components/ui/Question/data-grid";
import { SearchInput } from "@/components/ui/search";
import { useFetchData } from "@/hooks/use-fetch-data";
-const Index = () => {
+export default function Index() {
// Fetches the list of questions using the custom hook.
const {
data: questions,
@@ -50,9 +50,7 @@ const Index = () => {
};
// Displays a loading state while data is being fetched.
- if (isQuestionLoading) {
- return Loading...
;
- }
+ if (isQuestionLoading) return ;
// Displays an error message if the API request fails.
if (isQuestionError) {
@@ -84,10 +82,4 @@ const Index = () => {
>
);
-};
-
-Index.getLayout = function getLayout(page: React.ReactElement) {
- return {page};
-};
-
-export default Index;
+}
diff --git a/client/src/pages/users/index.tsx b/client/src/pages/users/index.tsx
index 5f807b8..7d294b2 100644
--- a/client/src/pages/users/index.tsx
+++ b/client/src/pages/users/index.tsx
@@ -2,6 +2,7 @@ import Link from "next/link";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
+import { WaitingLoader } from "@/components/ui/loading";
import { SearchInput } from "@/components/ui/search";
import { DataGrid } from "@/components/ui/Users/data-grid";
import { useFetchData } from "@/hooks/use-fetch-data";
@@ -47,7 +48,7 @@ export default function UserList() {
setPage(1);
};
- if (isUserLoading) return Loading...
;
+ if (isUserLoading) return ;
if (isUserError) return Error: {UserError?.message}
;
return (
diff --git a/client/src/pages/users/school.tsx b/client/src/pages/users/school.tsx
index 6cbc1d4..2395a99 100644
--- a/client/src/pages/users/school.tsx
+++ b/client/src/pages/users/school.tsx
@@ -2,6 +2,7 @@ import Link from "next/link";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
+import { WaitingLoader } from "@/components/ui/loading";
import { SearchInput } from "@/components/ui/search";
import { SchoolDataGrid } from "@/components/ui/Users/school-data-grid";
import { useFetchData } from "@/hooks/use-fetch-data";
@@ -47,7 +48,7 @@ export default function SchoolList() {
setPage(1);
};
- if (isSchoolLoading) return Loading...
;
+ if (isSchoolLoading) return ;
if (isSchoolError) return Error: {schoolError?.message}
;
return (
diff --git a/client/src/pages/users/team.tsx b/client/src/pages/users/team.tsx
index 75e93f6..4122564 100644
--- a/client/src/pages/users/team.tsx
+++ b/client/src/pages/users/team.tsx
@@ -2,6 +2,7 @@ import Link from "next/link";
import React, { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
+import { WaitingLoader } from "@/components/ui/loading";
import { SearchInput } from "@/components/ui/search";
import { TeamDataGrid } from "@/components/ui/Users/team-data-grid";
import { useFetchData } from "@/hooks/use-fetch-data";
@@ -47,7 +48,7 @@ export default function TeamList() {
setPage(1);
};
- if (isTeamLoading) return Loading...
;
+ if (isTeamLoading) return ;
if (isTeamError) return Error: {TeamError?.message}
;
return (
diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css
index 058fb96..2015f1c 100644
--- a/client/src/styles/globals.css
+++ b/client/src/styles/globals.css
@@ -162,6 +162,15 @@
font-weight: 600;
line-height: 24px;
}
+ /* Hide scrollbar for Chrome, Safari and Opera */
+ .no-scrollbar::-webkit-scrollbar {
+ display: none;
+ }
+ /* Hide scrollbar for IE, Edge and Firefox */
+ .no-scrollbar {
+ -ms-overflow-style: none; /* IE and Edge */
+ scrollbar-width: none; /* Firefox */
+ }
}
@layer base {