feat: fetch users list

This commit is contained in:
Sergey Yarkov 2021-09-15 20:56:59 +03:00
parent b0155d1ab3
commit 4ab5488f91
12 changed files with 188 additions and 84 deletions

View File

@ -3,7 +3,7 @@ import React from 'react';
const LoadingPage: React.FC = () => { const LoadingPage: React.FC = () => {
return ( return (
<Flex justifyContent='center' mt='20' maxW='950px'> <Flex justifyContent='center' mt='40' maxW='950px'>
<Box mt='auto' mb='auto' textAlign='center'> <Box mt='auto' mb='auto' textAlign='center'>
<Spinner thickness='4px' speed='0.55s' emptyColor='gray.200' color='blue.500' size='xl' /> <Spinner thickness='4px' speed='0.55s' emptyColor='gray.200' color='blue.500' size='xl' />
<Text mt='2' color='gray.500' userSelect='none'> <Text mt='2' color='gray.500' userSelect='none'>

View File

@ -57,12 +57,18 @@ export type UserCourseType = Pick<
export interface IUser { export interface IUser {
id: string; id: string;
fullname: string;
first_name: string; first_name: string;
last_name: string; last_name: string;
email: string; email: string;
roles: IUserRole[]; roles: IUserRole[];
contacts: IUserContacts | null; contacts: IUserContacts | null;
}
/**
* Current authenticated user data
*/
export interface IMe extends IUser {
fullname: string;
/** /**
* The field can be undefined if the user * The field can be undefined if the user
* does not have the STUDENT role * does not have the STUDENT role
@ -89,6 +95,12 @@ export interface IDataResult {
status: number; status: number;
} }
export interface IApiRespose<T> {
data: T;
message: string;
status: number;
}
export interface ILoginResult extends IDataResult { export interface ILoginResult extends IDataResult {
data: { data: {
token: string; token: string;

View File

@ -10,7 +10,7 @@ type CourseItemPropsType = {
const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => { const CourseItem: React.FC<CourseItemPropsType> = ({ course }) => {
return ( return (
<Box key={course.id} borderWidth='1px' borderRadius='md' p='10px' boxShadow='sm'> <Box borderWidth='1px' borderRadius='md' p='10px' boxShadow='sm'>
<Flex justifyContent='space-between' alignItems='center'> <Flex justifyContent='space-between' alignItems='center'>
<Link as={ReactRouterLink} to={`course/${course.id}`} fontWeight='bold' fontSize='2xl'> <Link as={ReactRouterLink} to={`course/${course.id}`} fontWeight='bold' fontSize='2xl'>
{course.title} {course.title}

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import { IUser } from 'interfaces'; import { IMe } from 'interfaces';
import { Box, Text, Stack, StackDivider } from '@chakra-ui/layout'; import { Box, Text, Stack, StackDivider } from '@chakra-ui/layout';
import CourseItem from './CourseItem'; import CourseItem from './CourseItem';
type CourseListPropsType = { type CourseListPropsType = {
courses: IUser['courses']; courses: IMe['courses'];
}; };
/** /**

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Box, Skeleton, SkeletonCircle, Flex, Spinner } from '@chakra-ui/react'; import { Box, Skeleton, SkeletonCircle, Flex, Spinner } from '@chakra-ui/react';
const LoadingPage = () => { const LoadingPage: React.FC = () => {
return ( return (
<Box maxW='900px'> <Box maxW='900px'>
<Skeleton height='35px' width='250px' borderRadius='md' /> <Skeleton height='35px' width='250px' borderRadius='md' />

View File

@ -0,0 +1,11 @@
import { Flex } from '@chakra-ui/layout';
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>;
};
export default LoadingPage;

View File

@ -0,0 +1,37 @@
import React from 'react';
import { Box, Flex, Avatar, Text, Tooltip, IconButton } from '@chakra-ui/react';
import { MdMoreHoriz } from 'react-icons/md';
import UserBadge from 'components/UserBadge';
import { IUser } from 'interfaces';
type UserItemPropsType = {
user: IUser;
};
const UserItem: React.FC<UserItemPropsType> = ({ user }) => {
return (
<Box borderWidth='1px' borderRadius='lg' w='full' p='3'>
<Flex alignItems='center' justifyContent='space-between' sx={{ gap: '20px' }}>
<Flex alignItems='center' flexBasis='230px'>
<Avatar name={`${user.first_name} ${user.last_name}`} size='sm' mr='3' />
<Box>
<Text fontSize='md' fontWeight='medium'>
{user.first_name}&nbsp;{user.last_name}
</Text>
<Text fontSize='sm'>{user.email}</Text>
</Box>
</Flex>
<Flex sx={{ gap: '5px' }}>
<UserBadge roles={user.roles} />
</Flex>
<Box>
<Tooltip label='Edit user' openDelay={400}>
<IconButton aria-label='Actions' variant='ghost' icon={<MdMoreHoriz />} />
</Tooltip>
</Box>
</Flex>
</Box>
);
};
export default UserItem;

View File

@ -0,0 +1,28 @@
import { Box, Stack, Text } from '@chakra-ui/layout';
import { IUser } from 'interfaces';
import React from 'react';
import UserItem from './UserItem';
type UserListPropsType = {
users: IUser[];
};
const UserList: React.FC<UserListPropsType> = ({ users }) => {
if (users.length === 0) {
return (
<Box textAlign='center'>
<Text>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>
);
};
export default UserList;

View File

@ -0,0 +1,3 @@
import UserList from './UserList';
export default UserList;

View File

@ -1,24 +1,46 @@
import React from 'react'; import React, { useEffect } from 'react';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import { Select } from '@chakra-ui/react'; import { Select } from '@chakra-ui/react';
import { Box, Flex, Text, Button, Heading, Badge, Avatar, Stack, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Text, Button, Heading } from '@chakra-ui/react';
import { Input, InputGroup, InputLeftElement, IconButton } from '@chakra-ui/react'; import { Input, InputGroup, InputLeftElement } from '@chakra-ui/react';
import { IPageProps } from 'interfaces';
import { AddIcon, ChevronLeftIcon, ChevronRightIcon, SearchIcon } from '@chakra-ui/icons'; import { AddIcon, ChevronLeftIcon, ChevronRightIcon, SearchIcon } from '@chakra-ui/icons';
import { MdMoreHoriz } from 'react-icons/md';
import { FaFileExcel } from 'react-icons/fa'; import { FaFileExcel } from 'react-icons/fa';
/**
* Types
*/
import { IPageProps } from 'interfaces';
/**
* Components
*/
import UserList from './components/UserList';
import LoadingPage from './components/LoadingPage';
/**
*Hooks
*/
import { useRootStore } from 'hooks/useRootStore';
import { useErrorHandler } from 'react-error-boundary';
/** /**
* Users Page * Users Page
*/ */
const UsersPage: React.FC<IPageProps> = ({ title }) => { const UsersPage: React.FC<IPageProps> = ({ title }) => {
const { userStore } = useRootStore();
const handleError = useErrorHandler();
useEffect(() => {
userStore.loadUsersData().catch(error => handleError(error));
}, [handleError, userStore]);
return ( return (
<Box> <Box h='full'>
<Heading as='h1'>User management</Heading> <Heading as='h1'>User management</Heading>
<Box mt='5'> {userStore.users !== null ? (
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'> <Flex mt='5' flexDir='column' h='full'>
<Box flexBasis='600px'> <Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
<Flex> <Flex flexBasis='600px'>
<Select defaultValue='any' w='full' mr='2'> <Select defaultValue='any' w='full' mr='2'>
<option value='any'>Any role</option> <option value='any'>Any role</option>
<option value='administrator'>Administrator</option> <option value='administrator'>Administrator</option>
@ -30,65 +52,39 @@ const UsersPage: React.FC<IPageProps> = ({ title }) => {
<Input placeholder='Search for a user...' /> <Input placeholder='Search for a user...' />
</InputGroup> </InputGroup>
</Flex> </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>
</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> </Box>
<Flex sx={{ gap: '10px' }}> <Flex mt='auto' mb='16' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
<Button variant='outline' color='blue.500' leftIcon={<AddIcon />}> <Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
Create new Page <b>1</b> of 23
</Button> </Box>
<Button variant='outline' colorScheme='green' leftIcon={<FaFileExcel />}> <Flex ml='auto' mr='auto' alignItems='center' sx={{ gap: '30px' }}>
Import <Button variant='link' leftIcon={<ChevronLeftIcon />}>
</Button> Previous page
</Button>
<Button variant='link' rightIcon={<ChevronRightIcon />}>
Next page
</Button>
</Flex>
</Flex> </Flex>
</Flex> </Flex>
<Box> ) : (
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'> <LoadingPage />
<Text>Total users: (24)</Text> )}
<Text>Actions</Text>
</Flex>
<Stack mt='4' spacing='2'>
{[1, 2, 3, 4, 5, 6, 7].map(e => (
<Box key={e} borderWidth='1px' borderRadius='lg' w='full' p='3'>
<Flex alignItems='center' justifyContent='space-between' sx={{ gap: '20px' }}>
<Flex alignItems='center'>
<Avatar name='Sergey Yarkov' size='sm' mr='3' />
<Box>
<Text fontSize='md' fontWeight='medium'>
Sergey Yarkov
</Text>
<Text fontSize='sm' color='gray.600'>
serzh.yarkov@gmail.com
</Text>
</Box>
</Flex>
<Flex sx={{ gap: '5px' }}>
<Badge variant='subtle' colorScheme='purple'>
Administrator
</Badge>
</Flex>
<Box>
<Tooltip label='Edit user' openDelay={400}>
<IconButton aria-label='Actions' variant='ghost' icon={<MdMoreHoriz />} />
</Tooltip>
</Box>
</Flex>
</Box>
))}
</Stack>
</Box>
<Flex mt='5' 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>
</Box>
</Box> </Box>
); );
}; };

View File

@ -1,11 +1,13 @@
import { AxiosInstance } from 'axios'; import { AxiosInstance } from 'axios';
import { import {
IApiRespose,
IDataResult, IDataResult,
IMe,
ISentCodeResult, ISentCodeResult,
IUpdatedContactsResult, IUpdatedContactsResult,
IUpdatedUserEmail, IUpdatedUserEmail,
IUser,
IUserContacts, IUserContacts,
IUserResult,
} from 'interfaces'; } from 'interfaces';
import * as helpers from 'helpers'; import * as helpers from 'helpers';
@ -16,11 +18,16 @@ export default class UserService {
this.api = api; this.api = api;
} }
public async fetchMe(): Promise<IUserResult> { public async fetchMe(): Promise<IApiRespose<IMe>> {
const result = await this.api.get('v1/me'); const result = await this.api.get('v1/me');
return result.data; return result.data;
} }
public async fetchAll(): Promise<IApiRespose<IUser[]>> {
const result = await this.api.get('/v1/users');
return result.data;
}
public async updateContacts(data: IUserContacts): Promise<IUpdatedContactsResult> { public async updateContacts(data: IUserContacts): Promise<IUpdatedContactsResult> {
const result = await this.api.put('v1/me/contacts', helpers.removeEmptyValues(data)); const result = await this.api.put('v1/me/contacts', helpers.removeEmptyValues(data));
return result.data; return result.data;

View File

@ -1,13 +1,13 @@
import { AxiosInstance } from 'axios'; import { AxiosInstance } from 'axios';
import { makeAutoObservable, runInAction } from 'mobx'; import { makeAutoObservable, runInAction } from 'mobx';
import { import {
IUserResult,
IUser, IUser,
IUserContacts, IUserContacts,
IUpdatedContactsResult, IUpdatedContactsResult,
IDataResult, IDataResult,
ISentCodeResult, ISentCodeResult,
IUpdatedUserEmail, IUpdatedUserEmail,
IMe,
} from 'interfaces'; } from 'interfaces';
/** /**
@ -25,9 +25,9 @@ export default class UserStore {
public userService: UserService; public userService: UserService;
public me: IUser | null = null; public me: IMe | null = null;
public users: IUser[] = []; public users: IUser[] | null = null;
constructor(root: RootStore, api: AxiosInstance) { constructor(root: RootStore, api: AxiosInstance) {
this.root = root; this.root = root;
@ -35,19 +35,29 @@ export default class UserStore {
makeAutoObservable(this); makeAutoObservable(this);
} }
/** public async loadUsersData() {
* Initialize authorized user in store try {
* const result = await this.userService.fetchAll();
* @returns User result console.log(`[${this.constructor.name}]: ${result.message}`, result);
*/
public async loadCurrentUserData(): Promise<IUserResult> { runInAction(() => {
this.users = result.data;
});
return result;
} catch (error: any) {
throw error;
}
}
public async loadCurrentUserData() {
try { try {
const result = await this.userService.fetchMe(); const result = await this.userService.fetchMe();
const { const {
data: { id, first_name, last_name, email, roles, contacts, courses }, data: { id, first_name, last_name, email, roles, contacts, courses },
} = result; } = result;
console.log(`[UserStore]: ${result.message}`, result); console.log(`[${this.constructor.name}]: ${result.message}`, result);
runInAction(() => { runInAction(() => {
this.me = { this.me = {