mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 02:09:08 +03:00
feat: users pagination
This commit is contained in:
parent
38fdce747e
commit
3ab6cafb06
25
package-lock.json
generated
25
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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>;
|
||||
};
|
||||
|
||||
|
||||
@ -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 (
|
||||
<Stack mt='4' spacing='2'>
|
||||
{users.map(user => (
|
||||
<UserItem key={user.id} user={user} />
|
||||
))}
|
||||
</Stack>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user