From 06dd34e46992504171bb962e8ca65104af973e47 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Thu, 26 Sep 2024 00:08:40 -0400 Subject: [PATCH] add useTable hook, custom table component and refactor manage user page --- .../components/common/table/CustomTable.tsx | 35 +++ .../src/components/pages/ManageUserPage.tsx | 199 ++++++++++-------- frontend/src/hooks/useTable.ts | 68 ++++++ 3 files changed, 219 insertions(+), 83 deletions(-) create mode 100644 frontend/src/components/common/table/CustomTable.tsx create mode 100644 frontend/src/hooks/useTable.ts diff --git a/frontend/src/components/common/table/CustomTable.tsx b/frontend/src/components/common/table/CustomTable.tsx new file mode 100644 index 0000000..f2940c2 --- /dev/null +++ b/frontend/src/components/common/table/CustomTable.tsx @@ -0,0 +1,35 @@ +// takes in data and displays them in the component +import React from "react"; +import { + Table, + TableContainer, + TableBody, + TableFooter, + TableHead, + Paper, +} from "@mui/material"; + +interface ICustomTable { + data: T[]; + renderRow: (row: T) => React.ReactNode; + renderHead: () => React.ReactNode; + renderFooter: () => React.ReactNode; + getKey: (row: T) => string | number; +} + +export default function CustomTable(props: ICustomTable) { + const { data, renderRow, renderHead, renderFooter, getKey } = props; + return ( + + + {renderHead()} + + {data.map((row: T) => ( + {renderRow(row)} + ))} + + {renderFooter()} +
+
+ ); +} diff --git a/frontend/src/components/pages/ManageUserPage.tsx b/frontend/src/components/pages/ManageUserPage.tsx index 70d9354..b656ba3 100644 --- a/frontend/src/components/pages/ManageUserPage.tsx +++ b/frontend/src/components/pages/ManageUserPage.tsx @@ -1,21 +1,17 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import MenuItem from "@mui/material/MenuItem"; import Select, { SelectChangeEvent } from "@mui/material/Select"; import InputLabel from "@mui/material/InputLabel"; import { - TableContainer, - Paper, - TableBody, TableRow, TableCell, - TableFooter, TablePagination, - TableHead, Stack, FormControl, } from "@mui/material"; import TablePaginationActions from "@mui/material/TablePagination/TablePaginationActions"; -import { Table } from "react-bootstrap"; +import CustomTable from "../common/table/CustomTable"; +import useTable from "../../hooks/useTable"; import { Role } from "../../types/AuthTypes"; import UserAPIClient from "../../APIClients/UserAPIClient"; import { User } from "../../types/UserTypes"; @@ -23,33 +19,122 @@ import { User } from "../../types/UserTypes"; const ManageUser = (): React.ReactElement => { const [role, setRole] = useState("Administrator"); const [users, setUsers] = useState([]); - const [page, setPage] = useState(1); - const [usersPerPage, setUsersPerPage] = useState(10); + + const { + paginatedData, + handleChangePage, + handleChangeRowsPerPage, + pageSize, + page, + setSortField, + setSortAscending, + sortField, + } = useTable({ + data: users, + }); + useEffect(() => { async function getUsers() { const fetchedUsers = await UserAPIClient.getUsersByRole(role); setUsers(fetchedUsers); - setPage(0); + handleChangePage(0); } getUsers(); }, [role]); - const emptyRows = - page > 0 ? Math.max(0, (1 + page) * usersPerPage - users.length) : 0; + const setSortFieldAndSortDirection = useCallback( + (selectedSortField: keyof User) => { + if (selectedSortField === sortField) { + setSortAscending((prev) => !prev); + } else { + setSortField(selectedSortField); + setSortAscending(true); + } + }, + [setSortField, setSortAscending, sortField], + ); + + const changePage = useCallback( + (event: React.MouseEvent | null, newPage: number) => { + handleChangePage(newPage); + }, + [handleChangePage], + ); + + const changeRowsPerPage = useCallback( + (event: React.ChangeEvent) => { + handleChangeRowsPerPage(parseInt(event.target.value, 10)); + handleChangePage(0); + }, + [handleChangePage, handleChangeRowsPerPage], + ); + + const renderHead = useCallback(() => { + return ( + + setSortFieldAndSortDirection("email")}> + Email + + setSortFieldAndSortDirection("firstName")} + > + First Name + + setSortFieldAndSortDirection("lastName")} + > + Last Name + + + ); + }, [setSortFieldAndSortDirection]); + + const renderRow = useCallback((user: User) => { + return ( + + + {user.email} + + + {user.firstName} + + + {user.lastName} + + + ); + }, []); - const handleChangePage = ( - event: React.MouseEvent | null, - newPage: number, - ) => { - setPage(newPage); - }; + const renderFooter = useCallback(() => { + return ( + + + + ); + }, [changePage, changeRowsPerPage, page, pageSize, users.length]); - const handleChangeRowsPerPage = ( - event: React.ChangeEvent, - ) => { - setUsersPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; + const getKey = useCallback((user: User) => { + return user.id; + }, []); return ( @@ -69,65 +154,13 @@ const ManageUser = (): React.ReactElement => { Learner - - - - - Email - First Name - Last Name - - - - {(usersPerPage > 0 - ? users.slice( - page * usersPerPage, - page * usersPerPage + usersPerPage, - ) - : users - ).map((user) => ( - - - {user.email} - - - {user.firstName} - - - {user.lastName} - - - ))} - {emptyRows > 0 && ( - - - - )} - - - - - - -
-
+
); }; diff --git a/frontend/src/hooks/useTable.ts b/frontend/src/hooks/useTable.ts new file mode 100644 index 0000000..61cb7e7 --- /dev/null +++ b/frontend/src/hooks/useTable.ts @@ -0,0 +1,68 @@ +import { useState, useMemo } from "react"; + +// want to take in data and be able to paginate, sort and filter the data + +interface UseTableOptions { + data: T[]; + initialPageSize?: number; + initialSortField?: keyof T; + initialAscendingOrder?: boolean; + filterFunction?: (item: T) => boolean; +} + +export default function useTable({ + data, + initialPageSize = 10, + initialSortField, + initialAscendingOrder = true, + filterFunction = () => true, +}: UseTableOptions) { + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(initialPageSize); + const [sortField, setSortField] = useState( + initialSortField, + ); + const [sortAscending, setSortAscending] = useState(initialAscendingOrder); + + // first we filter the data, then we sort it, then paginate it + const filteredData = useMemo(() => { + return data.filter((item: T) => filterFunction(item)); + }, [data, filterFunction]); + + const sortedData = useMemo(() => { + if (!sortField) return filteredData; + return filteredData.sort((a, b) => { + const aValue = a[sortField]; + const bValue = b[sortField]; + + if (initialAscendingOrder) return aValue > bValue ? 1 : -1; + return aValue < bValue ? 1 : -1; + }); + }, [filteredData, sortField, initialAscendingOrder]); + + const paginatedData = useMemo(() => { + return sortedData.slice(page * pageSize, (1 + page) * pageSize); + }, [page, pageSize, sortedData]); + + const handleChangePage = (newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (rowsPerPage: number) => { + setPageSize(rowsPerPage); + setPage(0); + }; + + return { + paginatedData, + handleChangePage, + handleChangeRowsPerPage, + setSortField, + setSortAscending, + setPageSize, + sortField, + sortAscending, + pageSize, + page, + }; +}