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 ( +
+
+ + + + + + No. + + + Username* + + + Password* + + Minimum 8 characters with letters, numbers, and symbols + + + Email + + User Role* + + + School* + + + + + + {fields.map((field, index) => ( + + {/* No. Field */} + + {index + 1} + + + {/* Username Field */} + + ( + + + + + + + )} + /> + + + {/* Password Field */} + + ( + + + + + + + )} + /> + + + {/* Email Field */} + + ( + + + + + + + )} + /> + + + {/* User Role Field */} + + ( + + + + + + + )} + /> + + + {/* School Field */} + + ( + + + + + + + )} + /> + + + {/* Delete Button */} + + + + + ))} + +
+
+ + +
+
+ +
+ ); +} 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 ( +
+
+ + + + + + No. + + Name* + + + + + {fields.map((field, index) => ( + + + {index + 1} + + + ( + + + + + + + )} + /> + + + + + + ))} + +
+
+ + +
+
+ +
+ ); +} 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 ( +
+
+ + + + + + No. + + Name* + + Description* + + + School* + + + + + + {fields.map((field, index) => ( + + {/* No. Field */} + + {index + 1} + + + {/* Name Field */} + + ( + + + + + + + )} + /> + + + {/* Description Field */} + + ( + + +