mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 18:29:15 +03:00
feat: fetch users list
This commit is contained in:
parent
b0155d1ab3
commit
4ab5488f91
@ -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'>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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' />
|
||||||
|
|||||||
11
src/pages/users/components/LoadingPage.tsx
Normal file
11
src/pages/users/components/LoadingPage.tsx
Normal 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;
|
||||||
37
src/pages/users/components/UserList/UserItem.tsx
Normal file
37
src/pages/users/components/UserList/UserItem.tsx
Normal 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} {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;
|
||||||
28
src/pages/users/components/UserList/UserList.tsx
Normal file
28
src/pages/users/components/UserList/UserList.tsx
Normal 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;
|
||||||
3
src/pages/users/components/UserList/index.ts
Normal file
3
src/pages/users/components/UserList/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import UserList from './UserList';
|
||||||
|
|
||||||
|
export default UserList;
|
||||||
@ -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 mt='5' flexDir='column' h='full'>
|
||||||
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
<Flex justifyContent='space-between' sx={{ gap: '10px' }} flexWrap='wrap'>
|
||||||
<Box flexBasis='600px'>
|
<Flex flexBasis='600px'>
|
||||||
<Flex>
|
|
||||||
<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,7 +52,6 @@ const UsersPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
<Input placeholder='Search for a user...' />
|
<Input placeholder='Search for a user...' />
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
|
||||||
<Flex sx={{ gap: '10px' }}>
|
<Flex sx={{ gap: '10px' }}>
|
||||||
<Button variant='outline' color='blue.500' leftIcon={<AddIcon />}>
|
<Button variant='outline' color='blue.500' leftIcon={<AddIcon />}>
|
||||||
Create new
|
Create new
|
||||||
@ -42,40 +63,12 @@ const UsersPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
|
<Flex mt='7' p='0 10px' fontWeight='bold' alignItems='center' justifyContent='space-between'>
|
||||||
<Text>Total users: (24)</Text>
|
<Text>Total users: ({userStore.users.length})</Text>
|
||||||
<Text>Actions</Text>
|
<Text>Actions</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Stack mt='4' spacing='2'>
|
<UserList users={userStore.users} />
|
||||||
{[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>
|
</Box>
|
||||||
</Flex>
|
<Flex mt='auto' mb='16' flexDirection={{ base: 'column', sm: 'column', md: 'row' }}>
|
||||||
<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' }}>
|
<Box textAlign={{ base: 'center', sm: 'center', md: 'left' }} mb={{ base: '2', md: '0' }}>
|
||||||
Page <b>1</b> of 23
|
Page <b>1</b> of 23
|
||||||
</Box>
|
</Box>
|
||||||
@ -88,7 +81,10 @@ const UsersPage: React.FC<IPageProps> = ({ title }) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Box>
|
</Flex>
|
||||||
|
) : (
|
||||||
|
<LoadingPage />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user