feat: users pagination

This commit is contained in:
Sergey Yarkov 2021-09-16 20:36:16 +03:00
parent 38fdce747e
commit 3ab6cafb06
10 changed files with 166 additions and 57 deletions

25
package-lock.json generated
View File

@ -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",

View File

@ -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",

View File

@ -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<IToken> | null;
error: any;
loading: boolean;
fetched: boolean;

View File

@ -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<T> {
data: T;
meta?: {
pagination?: IPaginationMeta;
};
message: string;
status: number;
}

View File

@ -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 <Flex>Loading users...</Flex>;
};

View File

@ -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<UserListPropsType> = ({ users }) => {
if (users.length === 0) {
const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
const { userStore } = useRootStore();
const handleError = useErrorHandler();
const [loading, setLoading] = useState<boolean>(false);
const pagesCount = Math.round(pagination.total / pagination.per_page);
const [pages] = useState<number>(pagesCount < 1 ? 1 : pagesCount);
const [currentPage, setCurrentPage] = useState<number>(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 (
<Box textAlign='center'>
<Text>There are no users in the system</Text>
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>There are no users in the system</Text>
</Box>
);
}
return (
<>
<Box>
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
<Text>Total users: ({pagination.total})</Text>
<Text>Actions</Text>
</Flex>
{loading ? (
<Box textAlign='center' mt='10' userSelect='none'>
<Text color='gray.500'>Wait a second...</Text>
</Box>
) : (
<Stack mt='4' spacing='2'>
{users.map(user => (
<UserItem key={user.id} user={user} />
))}
</Stack>
)}
</Box>
<Flex mt='auto' mb='16' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
Page <b>{currentPage}</b> of {pages}
</Box>
<Flex ml='auto' mr='auto' alignItems='center' sx={{ gap: '30px' }}>
<Button onClick={prevPage} disabled={currentPage <= 1} variant='link' leftIcon={<ChevronLeftIcon />}>
Previous page
</Button>
<Button onClick={nextPage} disabled={currentPage === pages} variant='link' rightIcon={<ChevronRightIcon />}>
Next page
</Button>
</Flex>
</Flex>
</>
);
};

View File

@ -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<IPageProps> = ({ 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 (
<Box h='full'>
@ -61,26 +63,9 @@ const UsersPage: React.FC<IPageProps> = ({ title }) => {
</Button>
</Flex>
</Flex>
<Box>
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
<Text>Total users: ({userStore.users.length})</Text>
<Text>Actions</Text>
</Flex>
<UserList users={userStore.users} />
</Box>
<Flex mt='auto' mb='16' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
Page <b>1</b> of 23
</Box>
<Flex ml='auto' mr='auto' alignItems='center' sx={{ gap: '30px' }}>
<Button variant='link' leftIcon={<ChevronLeftIcon />}>
Previous page
</Button>
<Button variant='link' rightIcon={<ChevronRightIcon />}>
Next page
</Button>
</Flex>
</Flex>
{userStore.users.meta.pagination && (
<UserList users={userStore.users.data} pagination={userStore.users.meta.pagination} />
)}
</Flex>
) : (
<LoadingPage />

View File

@ -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<IApiRespose<{ token: string; type: string; expires_at: string }>> {
public async requestLogin(login: string, password: string): Promise<IApiRespose<IToken>> {
const result = await this.api.post('v1/auth/login', {
login,
password,

View File

@ -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<IApiRespose<IUser[]>> {
const result = await this.api.get('/v1/users');
public async fetchAll(params?: FetchUsersParamsType): Promise<IApiRespose<IUser[]>> {
const { page, limit } = params || {};
const result = await this.api.get('/v1/users', {
params: {
page,
limit,
},
});
return result.data;
}

View File

@ -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;