From b91dc2e0ec0a7bf06c304f54f22bbb42c3c2476b Mon Sep 17 00:00:00 2001 From: Mahmod Date: Mon, 1 Jan 2024 14:37:05 +0300 Subject: [PATCH] some updates --- components/Initial-loading/index.tsx | 4 +- components/auth/confirm-email/index.tsx | 3 +- components/info-account.tsx | 5 + components/layouts/main/index.tsx | 14 +- components/table/components/body-loading.tsx | 26 +++ components/table/components/body.tsx | 69 ++++++ components/table/components/footer.tsx | 47 ++++ components/table/components/header.tsx | 43 ++++ components/table/components/pagination.tsx | 84 +++++++ components/table/index.tsx | 232 +++++++++++++++++++ components/table/type.ts | 68 ++++++ customization/components/join.css | 97 ++++++++ customization/components/skeleton.css | 29 +++ customization/components/table.css | 59 +++++ customization/utilities/styled/join.css | 6 + hooks/useDebounce.ts | 17 ++ hooks/useSocketIoClient.ts | 22 ++ lib/auth.ts | 2 +- lib/axios/interceptors.ts | 7 +- lib/socket-client.ts | 88 +++++++ middleware/auth.ts | 20 +- package-lock.json | 176 +++++++++++++- package.json | 3 + pages/_app.tsx | 23 +- pages/about.tsx | 5 - pages/index.tsx | 18 ++ pages/users-management.tsx | 71 ++++++ providers/SocketIoProvider.tsx | 62 +++++ services/auth/index.ts | 19 +- services/auth/type.ts | 4 + services/types.ts | 28 +++ services/users/index.ts | 14 ++ services/users/type.ts | 9 + store/index.ts | 6 + store/users/index.ts | 58 +++++ store/users/type.ts | 6 + utils/date.ts | 16 +- utils/hasRole.ts | 5 + utils/helper.ts | 17 ++ 39 files changed, 1439 insertions(+), 43 deletions(-) create mode 100644 components/table/components/body-loading.tsx create mode 100644 components/table/components/body.tsx create mode 100644 components/table/components/footer.tsx create mode 100644 components/table/components/header.tsx create mode 100644 components/table/components/pagination.tsx create mode 100644 components/table/index.tsx create mode 100644 components/table/type.ts create mode 100644 customization/components/join.css create mode 100644 customization/components/skeleton.css create mode 100644 customization/components/table.css create mode 100644 customization/utilities/styled/join.css create mode 100644 hooks/useDebounce.ts create mode 100644 hooks/useSocketIoClient.ts create mode 100644 lib/socket-client.ts delete mode 100644 pages/about.tsx create mode 100644 pages/users-management.tsx create mode 100644 providers/SocketIoProvider.tsx create mode 100644 services/types.ts create mode 100644 services/users/index.ts create mode 100644 services/users/type.ts create mode 100644 store/users/index.ts create mode 100644 store/users/type.ts create mode 100644 utils/hasRole.ts create mode 100644 utils/helper.ts diff --git a/components/Initial-loading/index.tsx b/components/Initial-loading/index.tsx index c850b24..84da8ef 100644 --- a/components/Initial-loading/index.tsx +++ b/components/Initial-loading/index.tsx @@ -1,9 +1,9 @@ const InitialLoading = () => { return ( -
+
- Auth Portal + Users Management
diff --git a/components/auth/confirm-email/index.tsx b/components/auth/confirm-email/index.tsx index f48d39b..8cdb8f8 100644 --- a/components/auth/confirm-email/index.tsx +++ b/components/auth/confirm-email/index.tsx @@ -10,8 +10,9 @@ const ConfirmEmail = () => { const hash = route.query?.hash; const { isLoading, isSuccess, mutate: confirm, isError } = useEmailConfirm(); useEffect(() => { + console.log(route.query); if (hash) confirm({ hash: hash }); - }, []); + }, [confirm, hash, route.query]); const goToHome = () => { location.replace("/"); }; diff --git a/components/info-account.tsx b/components/info-account.tsx index b2f0e3c..4ae989c 100644 --- a/components/info-account.tsx +++ b/components/info-account.tsx @@ -1,4 +1,5 @@ import { User } from "@/services/auth/type"; +import { formattedRoles } from "@/store/users"; import { formatDate } from "@/utils/date"; import { BsFillInfoSquareFill } from "react-icons/bs"; type PropsComponent = { @@ -49,6 +50,10 @@ const InfoAccount = ({ user }: PropsComponent) => { Join Data :{" "} {formatDate(user?.createdAt)}
+
+ Roles :{" "} + {user && formattedRoles(user.roles)} +
); diff --git a/components/layouts/main/index.tsx b/components/layouts/main/index.tsx index 758604f..21e003d 100644 --- a/components/layouts/main/index.tsx +++ b/components/layouts/main/index.tsx @@ -1,18 +1,26 @@ import InitialLoading from "@/components/Initial-loading"; +import { ProvideSocketIoClient } from "@/providers/SocketIoProvider"; import { useInfSession } from "@/services/auth"; import { selectors } from "@/store"; +import { useRouter } from "next/router"; import { ReactElement, useEffect } from "react"; type componentProps = { children: ReactElement; }; const Main = ({ children }: componentProps) => { - const { isLoading, data, isSuccess } = useInfSession(); + const router = useRouter(); const setToken = selectors.auth.setToken(); + const setUser = selectors.auth.setUser(); + const token = selectors.auth.token(); + const { data, isSuccess } = useInfSession({ + enabled: router.isReady && !token, + }); useEffect(() => { if (isSuccess) { setToken(data.token); + setUser(data.user); } }, [isSuccess]); return ( @@ -21,12 +29,12 @@ const Main = ({ children }: componentProps) => {
- {isLoading && ( + {!token && (
)} - {!isLoading && children} + {token && {children}}
diff --git a/components/table/components/body-loading.tsx b/components/table/components/body-loading.tsx new file mode 100644 index 0000000..f2044d8 --- /dev/null +++ b/components/table/components/body-loading.tsx @@ -0,0 +1,26 @@ +import React, { FC } from "react"; +import { BodyLoadingPros } from "../type"; + +const BodyLoading: FC = ({ columns, limit }) => { + const generateRows = () => { + const rows = []; + + for (let i = 0; i < limit; i++) { + rows.push( + + {columns.map((column) => ( + +
+ + ))} + + ); + } + + return rows; + }; + + return {generateRows()}; +}; + +export default BodyLoading; diff --git a/components/table/components/body.tsx b/components/table/components/body.tsx new file mode 100644 index 0000000..14cb1f6 --- /dev/null +++ b/components/table/components/body.tsx @@ -0,0 +1,69 @@ +import React, { FC } from "react"; +import { BodyProps } from "../type"; +import { Column } from "@/services/types"; +import classNames from "classnames"; + +const Body: FC = ({ data, columns, actions, showFooter }) => { + const renderCellContent = (column: Column, row: any) => { + return <>{column.filed(row)}; + }; + + const renderCell = (column: Column, row: any, index: number) => { + const style = + column.styled && typeof column.styled === "function" + ? column.styled(row) + : column.styled || {}; + + return ( + + {renderCellContent(column, row)} + + ); + }; + + const renderRow = (row: any, rowIndex: number) => ( + + {columns.map((column, columnIndex) => + renderCell(column, row, columnIndex) + )} + {actions && actions.length && ( + + {actions.map((action) => ( + + ))} + + )} + + ); + + return ( + <> + {data.map((row, rowIndex) => renderRow(row, rowIndex))} + {showFooter && ( + + + {columns.map((column) => ( + {column.label} + ))} + + + )} + + ); +}; + +export default Body; diff --git a/components/table/components/footer.tsx b/components/table/components/footer.tsx new file mode 100644 index 0000000..28c95ca --- /dev/null +++ b/components/table/components/footer.tsx @@ -0,0 +1,47 @@ +import { FC } from "react"; +import Pagination from "./pagination"; +import { FooterProps } from "../type"; + +const Footer: FC = ({ + limit, + handleLimitChange, + totalPages, + page, + handleChangeParams, +}) => { + return ( + <> +
+ {!!totalPages && ( +
+ + +
+ )} +
+ {totalPages !== undefined && ( + <> + + + )} +
+
+ + ); +}; + +export default Footer; diff --git a/components/table/components/header.tsx b/components/table/components/header.tsx new file mode 100644 index 0000000..4f3e54f --- /dev/null +++ b/components/table/components/header.tsx @@ -0,0 +1,43 @@ +import { FC } from "react"; +import { HeaderProps } from "../type"; + +const Header: FC = ({ + columns, + handleSortChange, + sortBy, + sortOrder, + actions, +}) => { + return ( + <> + + + {columns.map((column) => ( + + {column.label} + {sortBy === column.name && ( + handleSortChange(column.name)} + className="cursor-pointer" + > + {sortOrder === "asc" ? " ▲" : " ▼"} + + )} + handleSortChange(column.name)} + className="cursor-pointer" + > + {sortBy !== column.name && " ▲"} + + + ))} + {actions && actions.length > 0 && ( + Actions + )} + + + + ); +}; + +export default Header; diff --git a/components/table/components/pagination.tsx b/components/table/components/pagination.tsx new file mode 100644 index 0000000..557d651 --- /dev/null +++ b/components/table/components/pagination.tsx @@ -0,0 +1,84 @@ +import { FC } from "react"; +import { PaginationProps } from "../type"; + +const Pagination: FC = ({ + totalPages, + page, + handlePageChange, +}) => { + const buttons = []; + const maxButtons = 5; + const startPage = Math.max(1, page - Math.floor(maxButtons / 2)); + + const pageChang = (newPage: number) => { + handlePageChange({ + page: newPage, + }); + }; + + const addEllipsis = (key: string, onClick: any) => { + buttons.push( + + ... + + ); + }; + + if (startPage > 1) { + buttons.push( + + ); + if (startPage > 2) { + addEllipsis("firstEllipsis", () => pageChang(startPage - maxButtons)); + } + } + + for ( + let i = 0; + i < maxButtons && + startPage + i <= Math.min(totalPages, startPage + maxButtons - 1); + i++ + ) { + const pageNumber = startPage + i; + buttons.push( + + ); + } + + if (startPage + maxButtons - 1 < totalPages) { + if (startPage + maxButtons < totalPages) { + addEllipsis("lastEllipsis", () => pageChang(startPage + maxButtons)); + } + buttons.push( + + ); + } + + return
{buttons}
; +}; + +export default Pagination; diff --git a/components/table/index.tsx b/components/table/index.tsx new file mode 100644 index 0000000..6157dc1 --- /dev/null +++ b/components/table/index.tsx @@ -0,0 +1,232 @@ +import React, { + forwardRef, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from "react"; +import { + ComponentProps, + MyComponentMethods, + propTypesAction, + propTypesClassName, + propTypesColumn, + propTypesFetchQuery, + propTypesFilter, + propTypesShowFooter, + propTypesStickyActions, + propTypesSyncRoute, +} from "./type"; +import { useSearchParams } from "next/navigation"; +import { useRouter } from "next/router"; +import { updateUrlWithObjectQueries } from "@/utils/helper"; +import { twMerge } from "tailwind-merge"; +import classNames from "classnames"; +import Footer from "./components/footer"; +import Body from "./components/body"; +import Header from "./components/header"; +import BodyLoading from "./components/body-loading"; +import { IPaginationOptions } from "@/services/types"; + +type IBaseParams = Omit; + +const baseParams: IBaseParams = { + page: 1, + limit: 10, + sortOrder: undefined, + sortBy: undefined, +}; + +const Table = forwardRef( + ( + { + columns, + fetchQuery, + syncRoute, + filter, + className, + showFooter, + stickyActions, + actions, + }, + ref + ) => { + const searchParams = useSearchParams(); + const router = useRouter(); + + const [params, setParams] = useState(() => { + const updatedParams: any = { ...baseParams, ...filter }; + if (syncRoute) { + searchParams.forEach((value, key) => { + if (updatedParams.hasOwnProperty(key)) { + updatedParams[key] = value; + } + }); + } + return updatedParams; + }); + const [data, setData] = useState([]); + const [total, setTotal] = useState(undefined); + const [totalPages, setTotalPages] = useState(undefined); + + const { + isLoading, + data: InfoTable, + isError, + } = fetchQuery({ + ...params, + ...filter, + total: true, + }); + + useEffect(() => { + setData(InfoTable?.data); + setTotal(InfoTable?.total); + }, [InfoTable]); + + useEffect(() => { + if (total !== undefined) { + setTotalPages(Math.ceil(total / params.limit)); + } + }, [total, params.limit, totalPages]); + + useEffect(() => { + if (syncRoute && filter) { + const url = updateUrlWithObjectQueries( + { ...params, ...filter }, + router.asPath + ); + window.history.replaceState(window.history.state, "", url); + } + }, [filter]); + + const handleLimitChange = (pageNumber: number) => { + setParams({ + ...params, + limit: pageNumber, + page: 1, + }); + asyncWithRoute({ limit: pageNumber, page: 1 }); + }; + + const handleChangeParams = (object: object) => { + setParams((prevParams) => ({ + ...prevParams, + ...object, + })); + asyncWithRoute(object); + }; + + const handleSortChange = (columnName: string) => { + const sortOrder = + params.sortBy === columnName + ? params.sortOrder === "asc" + ? "desc" + : "asc" + : "asc"; + setParams({ + ...params, + sortBy: columnName, + sortOrder, + page: 1, + }); + asyncWithRoute({ sortBy: columnName, sortOrder, page: 1 }); + }; + + const asyncWithRoute = (object: object) => { + if (syncRoute) { + const url = updateUrlWithObjectQueries( + { ...params, ...object }, + router.asPath + ); + window.history.replaceState( + { ...window.history.state, as: url, url: url }, + "", + url + ); + } + }; + + const classes = twMerge( + "overflow-auto overflow-x-auto table-scroll h-96", + classNames(className) + ); + const classesTable = twMerge( + "table table-xs table-pin-rows table-zebra", + classNames({ "table-pin-cols": stickyActions }) + ); + const updateRow = ( + callback: (value: T) => boolean, + object: Partial + ): void => { + if (data) { + const index = data.findIndex((value) => callback(value)); + if (index !== -1) { + setData((prevData) => { + if (!prevData) return; + const newData = [...prevData]; + const updatedElement = { ...newData[index], ...object }; + newData[index] = updatedElement; + + return newData; + }); + } + } + }; + useImperativeHandle(ref, () => ({ + updateRow, + })); + if (isError) { + return
Error loading data
; + } + + return ( + <> +
+ +
+ {isLoading && !data && ( + + )} + {!isLoading && data && ( + + )} +
+
+