From 3ab6cafb06a5c2090f25fdb08d4c375c3cbd4950 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Thu, 16 Sep 2021 20:36:16 +0300 Subject: [PATCH] feat: users pagination --- package-lock.json | 25 ++++- package.json | 2 + src/hooks/useLoginQuery.ts | 4 +- src/interfaces/index.ts | 24 +++++ src/pages/users/components/LoadingPage.tsx | 2 - .../users/components/UserList/UserList.tsx | 95 ++++++++++++++++--- src/pages/users/index.tsx | 35 ++----- src/services/AuthService.ts | 7 +- src/services/UserService.ts | 12 ++- src/stores/UserStore.ts | 17 ++-- 10 files changed, 166 insertions(+), 57 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e42bd4..d446c08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3016,6 +3016,12 @@ "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==" }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, "@types/react": { "version": "16.14.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.13.tgz", @@ -4406,6 +4412,11 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" } } }, @@ -6998,6 +7009,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -12282,9 +12298,12 @@ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" }, "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.1.tgz", + "integrity": "sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==", + "requires": { + "side-channel": "^1.0.4" + } }, "query-string": { "version": "4.3.4", diff --git a/package.json b/package.json index 72ea233..847e27a 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "js-cookie": "^3.0.0", "mobx": "^6.3.2", "mobx-react": "^7.2.0", + "qs": "^6.10.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", @@ -57,6 +58,7 @@ }, "devDependencies": { "@types/js-cookie": "^2.2.7", + "@types/qs": "^6.9.7", "@types/react": "^16.14.13", "@types/react-helmet": "^6.1.2", "@types/react-router-dom": "^5.1.8", diff --git a/src/hooks/useLoginQuery.ts b/src/hooks/useLoginQuery.ts index e62189f..0408300 100644 --- a/src/hooks/useLoginQuery.ts +++ b/src/hooks/useLoginQuery.ts @@ -1,12 +1,12 @@ import { useToast } from '@chakra-ui/react'; -import { ILoginResult } from 'interfaces'; +import { IApiRespose, IToken } from 'interfaces'; import { useState } from 'react'; import { useErrorHandler } from 'react-error-boundary'; import useIsMountedRef from './useIsMountedRef'; import { useRootStore } from './useRootStore'; type LoginStateType = { - result: ILoginResult | null; + result: IApiRespose | null; error: any; loading: boolean; fetched: boolean; diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index f48beba..21a3efe 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -10,6 +10,12 @@ export interface IAppConfig { links: LinkType[]; } +export interface IToken { + token: string; + type: string; + expires_at: string; +} + export interface ICourse { id: string; title: string; @@ -38,6 +44,21 @@ export interface ICategory { description: string; } +/** + * Pagination meta data that returns from server + */ +export interface IPaginationMeta { + current_page: number; + first_page: number; + first_page_url: string; + last_page: number; + last_page_url: string; + next_page_url: string | null; + per_page: number; + previous_page_url: string | null; + total: number; +} + /** * User with available course */ @@ -90,6 +111,9 @@ export interface IUserRole { export interface IApiRespose { data: T; + meta?: { + pagination?: IPaginationMeta; + }; message: string; status: number; } diff --git a/src/pages/users/components/LoadingPage.tsx b/src/pages/users/components/LoadingPage.tsx index dd33779..99f57bd 100644 --- a/src/pages/users/components/LoadingPage.tsx +++ b/src/pages/users/components/LoadingPage.tsx @@ -3,8 +3,6 @@ import React from 'react'; //TODO add some skeleton styling when loading this page const LoadingPage: React.FC = () => { - console.log('123'); - return Loading users...; }; diff --git a/src/pages/users/components/UserList/UserList.tsx b/src/pages/users/components/UserList/UserList.tsx index b545206..c3b9d81 100644 --- a/src/pages/users/components/UserList/UserList.tsx +++ b/src/pages/users/components/UserList/UserList.tsx @@ -1,27 +1,98 @@ -import { Box, Stack, Text } from '@chakra-ui/layout'; -import { IUser } from 'interfaces'; -import React from 'react'; +import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons'; +import { Flex, Box, Stack, Text } from '@chakra-ui/layout'; +import { Button } from '@chakra-ui/button'; +import { IPaginationMeta, IUser } from 'interfaces'; +import React, { useState } from 'react'; import UserItem from './UserItem'; +import { useRootStore } from 'hooks/useRootStore'; +import { useErrorHandler } from 'react-error-boundary'; type UserListPropsType = { users: IUser[]; + pagination: IPaginationMeta; }; -const UserList: React.FC = ({ users }) => { - if (users.length === 0) { +const UserList: React.FC = ({ users, pagination }) => { + const { userStore } = useRootStore(); + const handleError = useErrorHandler(); + const [loading, setLoading] = useState(false); + const pagesCount = Math.round(pagination.total / pagination.per_page); + const [pages] = useState(pagesCount < 1 ? 1 : pagesCount); + const [currentPage, setCurrentPage] = useState(pagination.current_page); + + const nextPage = async () => { + if (currentPage === pages) return; + try { + setLoading(true); + await userStore.loadUsersData({ + page: currentPage + 1, + limit: pagination.per_page, + }); + setCurrentPage(p => p + 1); + } catch (error: any) { + handleError(error); + } finally { + setLoading(false); + } + }; + + const prevPage = async () => { + if (currentPage <= 1) return; + try { + setLoading(true); + await userStore.loadUsersData({ + page: currentPage - 1, + limit: pagination.per_page, + }); + setCurrentPage(p => p - 1); + } catch (error: any) { + handleError(error); + } finally { + setLoading(false); + } + }; + + if (pagination.total === 0) { return ( - - There are no users in the system + + There are no users in the system ); } return ( - - {users.map(user => ( - - ))} - + <> + + + Total users: ({pagination.total}) + Actions + + {loading ? ( + + Wait a second... + + ) : ( + + {users.map(user => ( + + ))} + + )} + + + + Page {currentPage} of {pages} + + + + + + + ); }; diff --git a/src/pages/users/index.tsx b/src/pages/users/index.tsx index 0b2167c..137e189 100644 --- a/src/pages/users/index.tsx +++ b/src/pages/users/index.tsx @@ -1,9 +1,9 @@ import React, { useEffect } from 'react'; import { observer } from 'mobx-react'; import { Select } from '@chakra-ui/react'; -import { Box, Flex, Text, Button, Heading } from '@chakra-ui/react'; +import { Box, Flex, Button, Heading } from '@chakra-ui/react'; import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react'; -import { AddIcon, ChevronLeftIcon, ChevronRightIcon, SearchIcon } from '@chakra-ui/icons'; +import { AddIcon, SearchIcon } from '@chakra-ui/icons'; import { FaFileExcel } from 'react-icons/fa'; /** @@ -18,10 +18,11 @@ import UserList from './components/UserList'; import LoadingPage from './components/LoadingPage'; /** - *Hooks + * Hooks */ import { useRootStore } from 'hooks/useRootStore'; import { useErrorHandler } from 'react-error-boundary'; +import { useLocation } from 'react-router'; /** * Users Page @@ -29,10 +30,11 @@ import { useErrorHandler } from 'react-error-boundary'; const UsersPage: React.FC = ({ title }) => { const { userStore } = useRootStore(); const handleError = useErrorHandler(); + const location = useLocation(); useEffect(() => { - userStore.loadUsersData().catch(error => handleError(error)); - }, [handleError, userStore]); + userStore.loadUsersData({ page: 1, limit: 2 }).catch(error => handleError(error)); + }, [handleError, userStore, location.search]); return ( @@ -61,26 +63,9 @@ const UsersPage: React.FC = ({ title }) => { - - - Total users: ({userStore.users.length}) - Actions - - - - - - Page 1 of 23 - - - - - - + {userStore.users.meta.pagination && ( + + )} ) : ( diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index 8feea97..9493780 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -1,4 +1,4 @@ -import { IApiRespose } from 'interfaces'; +import { IApiRespose, IToken } from 'interfaces'; import { AxiosInstance } from 'axios'; export default class AuthService { @@ -15,10 +15,7 @@ export default class AuthService { * @param password User password * @returns Login result data */ - public async requestLogin( - login: string, - password: string - ): Promise> { + public async requestLogin(login: string, password: string): Promise> { const result = await this.api.post('v1/auth/login', { login, password, diff --git a/src/services/UserService.ts b/src/services/UserService.ts index 4b09fc7..5fecac3 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -2,6 +2,8 @@ import { AxiosInstance } from 'axios'; import { IApiRespose, IMe, IUser, IUserContacts } from 'interfaces'; import * as helpers from 'helpers'; +export type FetchUsersParamsType = { page?: number; limit?: number }; + export default class UserService { public api: AxiosInstance; @@ -14,8 +16,14 @@ export default class UserService { return result.data; } - public async fetchAll(): Promise> { - const result = await this.api.get('/v1/users'); + public async fetchAll(params?: FetchUsersParamsType): Promise> { + const { page, limit } = params || {}; + const result = await this.api.get('/v1/users', { + params: { + page, + limit, + }, + }); return result.data; } diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 1565b39..a1c2a0d 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -1,11 +1,11 @@ import { AxiosInstance } from 'axios'; import { makeAutoObservable, runInAction } from 'mobx'; -import { IUser, IUserContacts, IMe } from 'interfaces'; +import { IUser, IUserContacts, IMe, IPaginationMeta } from 'interfaces'; /** * Services */ -import UserService from 'services/UserService'; +import UserService, { FetchUsersParamsType } from 'services/UserService'; /** * Stores @@ -19,7 +19,7 @@ export default class UserStore { public me: IMe | null = null; - public users: IUser[] | null = null; + public users: { data: IUser[]; meta: { pagination?: IPaginationMeta } } | null = null; constructor(root: RootStore, api: AxiosInstance) { this.root = root; @@ -27,13 +27,18 @@ export default class UserStore { makeAutoObservable(this); } - public async loadUsersData() { + public async loadUsersData(params?: FetchUsersParamsType) { try { - const result = await this.userService.fetchAll(); + const result = await this.userService.fetchAll(params); console.log(`[${this.constructor.name}]: ${result.message}`, result); runInAction(() => { - this.users = result.data; + this.users = { + data: result.data, + meta: { + pagination: result.meta?.pagination, + }, + }; }); return result;