mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: users pagination & searching users by role
This commit is contained in:
parent
3ab6cafb06
commit
a4f3aebec5
@ -12,6 +12,7 @@ const UserBadge: React.FC<{ roles: IUserRole[] }> = ({ roles }) => {
|
||||
if (userHasRoles(roles, [UserRoleEnum.ADMIN])) return { name: 'Administrator', color: 'purple' };
|
||||
if (userHasRoles(roles, [UserRoleEnum.TEACHER])) return { name: 'Teacher', color: 'orange' };
|
||||
if (userHasRoles(roles, [UserRoleEnum.STUDENT])) return { name: 'Student', color: 'blue' };
|
||||
if (roles.length === 0) return { name: 'Guest', color: 'gray' };
|
||||
};
|
||||
|
||||
const role = getCurrentRole();
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import React from 'react';
|
||||
import { ProfilePageViewStatusContextType } from 'types';
|
||||
import { createContext } from 'react';
|
||||
import { ProfilePageViewStatusContextType, UsersPageContextType } from 'types';
|
||||
import RootStore from '../stores/RootStore';
|
||||
|
||||
export const StoreContext = React.createContext<RootStore | undefined>(undefined);
|
||||
export const StoreContext = createContext<RootStore | undefined>(undefined);
|
||||
|
||||
export const ProfilePageViewContext = React.createContext<ProfilePageViewStatusContextType>({
|
||||
export const ProfilePageViewContext = createContext<ProfilePageViewStatusContextType>({
|
||||
statusPageView: 'default',
|
||||
setStatusPageView: () => {},
|
||||
});
|
||||
|
||||
export const UsersPageContext = createContext<UsersPageContextType>({
|
||||
searchingRole: undefined,
|
||||
setSearchingRole: () => {},
|
||||
loading: false,
|
||||
setLoading: () => {},
|
||||
});
|
||||
|
||||
@ -112,6 +112,9 @@ export interface IUserRole {
|
||||
export interface IApiRespose<T> {
|
||||
data: T;
|
||||
meta?: {
|
||||
/**
|
||||
* The server can return the pagination meta data
|
||||
*/
|
||||
pagination?: IPaginationMeta;
|
||||
};
|
||||
message: string;
|
||||
|
||||
@ -75,6 +75,7 @@ const ProfilePage: React.FC<IPageProps> = () => {
|
||||
{statusPageView.status === 'default' && (
|
||||
<Box maxW='900px'>
|
||||
<Heading as='h1'>My account</Heading>
|
||||
<Text mt='2'>Update you contacts information or password here.</Text>
|
||||
<Flex
|
||||
alignItems='center'
|
||||
mt='20'
|
||||
|
||||
@ -2,10 +2,12 @@ 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 React, { useContext } from 'react';
|
||||
import UserItem from './UserItem';
|
||||
import { useRootStore } from 'hooks/useRootStore';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import { UsersPageContextType } from 'types';
|
||||
import { UsersPageContext } from 'contexts';
|
||||
|
||||
type UserListPropsType = {
|
||||
users: IUser[];
|
||||
@ -14,21 +16,18 @@ type UserListPropsType = {
|
||||
|
||||
const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
||||
const { userStore } = useRootStore();
|
||||
const { searchingRole, loading, setLoading } = useContext<UsersPageContextType>(UsersPageContext);
|
||||
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 pagesCount = Math.ceil(pagination.total / pagination.per_page);
|
||||
|
||||
const nextPage = async () => {
|
||||
if (currentPage === pages) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
await userStore.loadUsersData({
|
||||
page: currentPage + 1,
|
||||
page: pagination.current_page + 1,
|
||||
limit: pagination.per_page,
|
||||
role: searchingRole,
|
||||
});
|
||||
setCurrentPage(p => p + 1);
|
||||
} catch (error: any) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
@ -37,14 +36,13 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
||||
};
|
||||
|
||||
const prevPage = async () => {
|
||||
if (currentPage <= 1) return;
|
||||
try {
|
||||
setLoading(true);
|
||||
await userStore.loadUsersData({
|
||||
page: currentPage - 1,
|
||||
page: pagination.current_page - 1,
|
||||
limit: pagination.per_page,
|
||||
role: searchingRole,
|
||||
});
|
||||
setCurrentPage(p => p - 1);
|
||||
} catch (error: any) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
@ -52,46 +50,56 @@ const UserList: React.FC<UserListPropsType> = ({ users, pagination }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (pagination.total === 0) {
|
||||
return (
|
||||
<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>
|
||||
{pagination.total !== 0 ? (
|
||||
<>
|
||||
<Box>
|
||||
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
|
||||
<Text>Total: ({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>
|
||||
) : (
|
||||
<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}
|
||||
<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>{pagination.current_page}</b> of {pagesCount}
|
||||
</Box>
|
||||
<Flex ml='auto' mr='auto' alignItems='center' sx={{ gap: '30px' }}>
|
||||
<Button
|
||||
onClick={prevPage}
|
||||
disabled={pagination.current_page <= 1}
|
||||
variant='link'
|
||||
leftIcon={<ChevronLeftIcon />}
|
||||
>
|
||||
Previous page
|
||||
</Button>
|
||||
<Button
|
||||
onClick={nextPage}
|
||||
disabled={pagination.current_page === pagesCount}
|
||||
variant='link'
|
||||
rightIcon={<ChevronRightIcon />}
|
||||
>
|
||||
Next page
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
) : (
|
||||
<Box textAlign='center' mt='10' userSelect='none'>
|
||||
<Text color='gray.500'>Cannot find any users.</Text>
|
||||
</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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
53
src/pages/users/components/UserSearch/UserSearch.tsx
Normal file
53
src/pages/users/components/UserSearch/UserSearch.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Flex, Select, Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
|
||||
import { SearchIcon } from '@chakra-ui/icons';
|
||||
import { useRootStore } from 'hooks/useRootStore';
|
||||
import { UserRoleEnum } from 'enums';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import { UsersPageContext } from 'contexts';
|
||||
import { UsersPageContextType } from 'types';
|
||||
|
||||
type SelectRoleStateType = UserRoleEnum | 'any';
|
||||
|
||||
const UserSearch: React.FC = () => {
|
||||
const { userStore } = useRootStore();
|
||||
const { searchingRole, setSearchingRole, setLoading } = useContext<UsersPageContextType>(UsersPageContext);
|
||||
const { pagination } = userStore;
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
if (pagination === undefined) return null;
|
||||
|
||||
const onSearchRole: React.ChangeEventHandler<HTMLSelectElement> | undefined = async e => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const value = e.target.value as SelectRoleStateType;
|
||||
await userStore.loadUsersData({
|
||||
page: 1,
|
||||
limit: pagination.per_page,
|
||||
role: value,
|
||||
});
|
||||
setSearchingRole(value);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexBasis='600px'>
|
||||
<Select value={searchingRole} onChange={onSearchRole} w='full' mr='2'>
|
||||
<option value='any'>Any role</option>
|
||||
<option value={UserRoleEnum.ADMIN}>Administrator</option>
|
||||
<option value={UserRoleEnum.TEACHER}>Teacher</option>
|
||||
<option value={UserRoleEnum.STUDENT}>Student</option>
|
||||
</Select>
|
||||
<InputGroup maxW='400px' w='full'>
|
||||
<InputLeftElement pointerEvents='none' children={<SearchIcon color='gray.300' />} />
|
||||
<Input placeholder='Search for a user...' />
|
||||
</InputGroup>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSearch;
|
||||
3
src/pages/users/components/UserSearch/index.ts
Normal file
3
src/pages/users/components/UserSearch/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import UserSearch from './UserSearch';
|
||||
|
||||
export default UserSearch;
|
||||
@ -1,19 +1,19 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { Select } from '@chakra-ui/react';
|
||||
import { Box, Flex, Button, Heading } from '@chakra-ui/react';
|
||||
import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
|
||||
import { AddIcon, SearchIcon } from '@chakra-ui/icons';
|
||||
import { Box, Flex, Button, Heading, Text } from '@chakra-ui/react';
|
||||
import { AddIcon } from '@chakra-ui/icons';
|
||||
import { FaFileExcel } from 'react-icons/fa';
|
||||
|
||||
/**
|
||||
* Types
|
||||
*/
|
||||
import { IPageProps } from 'interfaces';
|
||||
import { SearchingRoleStateType } from 'types';
|
||||
|
||||
/**
|
||||
* Components
|
||||
*/
|
||||
import UserSearch from './components/UserSearch';
|
||||
import UserList from './components/UserList';
|
||||
import LoadingPage from './components/LoadingPage';
|
||||
|
||||
@ -24,53 +24,50 @@ import { useRootStore } from 'hooks/useRootStore';
|
||||
import { useErrorHandler } from 'react-error-boundary';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
/**
|
||||
* Context
|
||||
*/
|
||||
import { UsersPageContext } from 'contexts';
|
||||
|
||||
/**
|
||||
* Users Page
|
||||
*/
|
||||
const UsersPage: React.FC<IPageProps> = ({ title }) => {
|
||||
const { userStore } = useRootStore();
|
||||
const [searchingRole, setSearchingRole] = useState<SearchingRoleStateType>(undefined);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const handleError = useErrorHandler();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
userStore.loadUsersData({ page: 1, limit: 2 }).catch(error => handleError(error));
|
||||
userStore.loadUsersData().catch(error => handleError(error));
|
||||
}, [handleError, userStore, location.search]);
|
||||
|
||||
return (
|
||||
<Box h='full'>
|
||||
<Heading as='h1'>User management</Heading>
|
||||
{userStore.users !== null ? (
|
||||
<Flex mt='5' flexDir='column' h='full'>
|
||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||
<Flex flexBasis='600px'>
|
||||
<Select defaultValue='any' w='full' mr='2'>
|
||||
<option value='any'>Any role</option>
|
||||
<option value='administrator'>Administrator</option>
|
||||
<option value='teacher'>Teacher</option>
|
||||
<option value='student'>Student</option>
|
||||
</Select>
|
||||
<InputGroup maxW='400px' w='full'>
|
||||
<InputLeftElement pointerEvents='none' children={<SearchIcon color='gray.300' />} />
|
||||
<Input placeholder='Search for a user...' />
|
||||
</InputGroup>
|
||||
</Flex>
|
||||
<Flex sx={{ gap: '10px' }}>
|
||||
<Button variant='outline' color='blue.500' leftIcon={<AddIcon />}>
|
||||
Create new
|
||||
</Button>
|
||||
<Button variant='outline' colorScheme='green' leftIcon={<FaFileExcel />}>
|
||||
Import
|
||||
</Button>
|
||||
<UsersPageContext.Provider value={{ searchingRole, setSearchingRole, loading, setLoading }}>
|
||||
<Box h='full'>
|
||||
<Heading as='h1'>User management</Heading>
|
||||
<Text mt='2'>You can add or delete your users on this page.</Text>
|
||||
{userStore.users !== null ? (
|
||||
<Flex mt='5' flexDir='column' h='full'>
|
||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||
<UserSearch />
|
||||
<Flex sx={{ gap: '10px' }}>
|
||||
<Button variant='outline' color='blue.500' leftIcon={<AddIcon />}>
|
||||
Create new
|
||||
</Button>
|
||||
<Button variant='outline' colorScheme='green' leftIcon={<FaFileExcel />}>
|
||||
Import
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
{userStore.pagination && <UserList users={userStore.users} pagination={userStore.pagination} />}
|
||||
</Flex>
|
||||
{userStore.users.meta.pagination && (
|
||||
<UserList users={userStore.users.data} pagination={userStore.users.meta.pagination} />
|
||||
)}
|
||||
</Flex>
|
||||
) : (
|
||||
<LoadingPage />
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<LoadingPage />
|
||||
)}
|
||||
</Box>
|
||||
</UsersPageContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
import { IApiRespose, IMe, IUser, IUserContacts } from 'interfaces';
|
||||
import * as helpers from 'helpers';
|
||||
import { UserRoleEnum } from 'enums';
|
||||
|
||||
export type FetchUsersParamsType = { page?: number; limit?: number };
|
||||
export type FetchUsersParamsType = { page?: number; limit?: number; role?: UserRoleEnum | 'any' };
|
||||
|
||||
export default class UserService {
|
||||
public api: AxiosInstance;
|
||||
@ -17,11 +18,12 @@ export default class UserService {
|
||||
}
|
||||
|
||||
public async fetchAll(params?: FetchUsersParamsType): Promise<IApiRespose<IUser[]>> {
|
||||
const { page, limit } = params || {};
|
||||
const { page = 1, limit = 1, role } = params || {};
|
||||
const result = await this.api.get('/v1/users', {
|
||||
params: {
|
||||
page,
|
||||
limit,
|
||||
role: role === 'any' ? undefined : role,
|
||||
},
|
||||
});
|
||||
return result.data;
|
||||
|
||||
@ -19,7 +19,9 @@ export default class UserStore {
|
||||
|
||||
public me: IMe | null = null;
|
||||
|
||||
public users: { data: IUser[]; meta: { pagination?: IPaginationMeta } } | null = null;
|
||||
public users: IUser[] | null = null;
|
||||
|
||||
public pagination: IPaginationMeta | undefined;
|
||||
|
||||
constructor(root: RootStore, api: AxiosInstance) {
|
||||
this.root = root;
|
||||
@ -33,12 +35,8 @@ export default class UserStore {
|
||||
console.log(`[${this.constructor.name}]: ${result.message}`, result);
|
||||
|
||||
runInAction(() => {
|
||||
this.users = {
|
||||
data: result.data,
|
||||
meta: {
|
||||
pagination: result.meta?.pagination,
|
||||
},
|
||||
};
|
||||
this.users = result.data;
|
||||
this.pagination = result.meta && result.meta.pagination;
|
||||
});
|
||||
|
||||
return result;
|
||||
@ -70,7 +68,7 @@ export default class UserStore {
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -92,7 +90,7 @@ export default class UserStore {
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -108,7 +106,7 @@ export default class UserStore {
|
||||
try {
|
||||
const result = await this.userService.updatePassword(oldPassword, newPassword);
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -123,7 +121,7 @@ export default class UserStore {
|
||||
try {
|
||||
const result = await this.userService.updateEmail(email);
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@ -144,7 +142,7 @@ export default class UserStore {
|
||||
}
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
16
src/types/index.d.ts
vendored
16
src/types/index.d.ts
vendored
@ -1,4 +1,5 @@
|
||||
import { UserRoleEnum } from 'enums';
|
||||
import React from 'react';
|
||||
import { IconType } from 'react-icons/lib';
|
||||
|
||||
export type LinkType = {
|
||||
@ -27,7 +28,22 @@ export type ProfilePageStatusType = {
|
||||
confirmEmailData?: ConfirmEmailContainerDataType;
|
||||
};
|
||||
};
|
||||
|
||||
export type ProfilePageViewStatusContextType = {
|
||||
statusPageView: ProfilePageStatusType['status'];
|
||||
setStatusPageView: React.Dispatch<React.SetStateAction<ProfilePageStatusType>>;
|
||||
};
|
||||
|
||||
export type SearchingRoleStateType = UserRoleEnum | 'any' | undefined;
|
||||
export type UsersPageContextType = {
|
||||
/**
|
||||
* Search users by user role
|
||||
*/
|
||||
searchingRole: SearchingRoleStateType;
|
||||
setSearchingRole: React.Dispatch<React.SetStateAction<SearchingRoleStateType>>;
|
||||
/**
|
||||
* Set loading state while loading users
|
||||
*/
|
||||
loading: boolean;
|
||||
setLoading: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user