feat: added chat window layout

This commit is contained in:
Sergey Yarkov 2022-02-08 23:53:27 +03:00
parent 682cbc6ce9
commit 464f58363b
24 changed files with 447 additions and 115 deletions

View File

@ -0,0 +1,10 @@
import React, { useState } from 'react';
import { ChatContext } from './context';
const ChatContextProvider: React.FC = ({ children }) => {
const [search, setSearch] = useState<string>('');
return <ChatContext.Provider value={{ search, setSearch }}>{children}</ChatContext.Provider>;
};
export { ChatContextProvider };

View File

@ -1,12 +0,0 @@
import React from 'react';
import { GridItem, GridItemProps } from '@chakra-ui/react';
const Sidebar: React.FC<GridItemProps> = props => {
return (
<GridItem pt='7rem' borderRightWidth={'1px'} width='400px' pr='6' {...props}>
{props.children}
</GridItem>
);
};
export { Sidebar };

View File

@ -0,0 +1,50 @@
import React from 'react';
import {
Flex,
Heading as ChakraHeading,
Input,
InputGroup,
InputLeftElement,
HStack,
Button,
Box,
BoxProps,
} from '@chakra-ui/react';
import { SearchIcon } from '@chakra-ui/icons';
/**
* Hooks
*/
import { useChatContext } from '../context';
const Heading: React.FC<BoxProps> = props => {
const { search, setSearch } = useChatContext();
const onSearch = (e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value);
return (
<Box {...props}>
<Flex my='4' pr={'3'} justifyContent={'space-between'} alignItems={'center'}>
<ChakraHeading as='h2' fontSize={'md'} fontWeight='medium' mr='9'>
Chats
</ChakraHeading>
<InputGroup maxW='230px' size='sm'>
<InputLeftElement pointerEvents='none'>
<SearchIcon color='gray.300' />
</InputLeftElement>
<Input type='text' onChange={onSearch} value={search} placeholder='Search by name...' borderRadius={'md'} />
</InputGroup>
</Flex>
<HStack>
<Button isActive variant={'ghost'} size='sm'>
All
</Button>
<Button colorScheme={'blue'} variant='ghost' size='sm'>
Unread
</Button>
</HStack>
</Box>
);
};
export { Heading };

View File

@ -0,0 +1,30 @@
import React from 'react';
import { BoxProps, GridItem, GridItemProps } from '@chakra-ui/react';
import { Heading } from './Heading';
import { Users } from './Users/Users';
export interface ISidebarComposition {
Heading: React.FC<BoxProps>;
Users: React.FC;
}
const Sidebar: React.FC<GridItemProps> & ISidebarComposition = props => {
return (
<GridItem
pt='7rem'
borderRightWidth={{ base: '0', lg: '1px' }}
width={{ base: 'full', lg: '400px' }}
px={{ base: '0', lg: '3' }}
{...props}
>
{props.children}
</GridItem>
);
return null;
};
Sidebar.Heading = Heading;
Sidebar.Users = Users;
export { Sidebar };

View File

@ -0,0 +1,21 @@
import React from 'react';
import { Avatar as ChakraAvatar, AvatarBadge, WrapItem } from '@chakra-ui/react';
export interface IAvatarProps {
fullname: string;
isOnline: boolean;
}
const Avatar: React.FC<IAvatarProps> = ({ fullname: name, isOnline }) => {
const onlineBg = isOnline ? 'green.500' : 'red.500';
return (
<WrapItem>
<ChakraAvatar name={name} size='md'>
<AvatarBadge boxSize='0.90em' bg={onlineBg} />
</ChakraAvatar>
</WrapItem>
);
};
export { Avatar };

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Box, Text } from '@chakra-ui/react';
export interface IInfoProps {
fullname: string;
lastMessage: string;
}
const Info: React.FC<IInfoProps> = ({ fullname, lastMessage }) => {
return (
<Box ml='4'>
<Text fontWeight={'semibold'}>
{fullname} <br />
<Text as={'span'} fontWeight='normal' fontSize={'sm'} color='gray.500'>
{lastMessage}
</Text>
</Text>
</Box>
);
};
export { Info };

View File

@ -0,0 +1,18 @@
import React from 'react';
import { Box, Text } from '@chakra-ui/react';
export interface ITimeProps {
time: string;
}
const Time: React.FC<ITimeProps> = ({ time }) => {
return (
<Box alignSelf={'flex-end'} ml='auto'>
<Text as='span' color='gray.500'>
{time}
</Text>
</Box>
);
};
export { Time };

View File

@ -0,0 +1,55 @@
import React from 'react';
import { Box, Flex } from '@chakra-ui/react';
import { IInfoProps, Info } from './Info';
import { ITimeProps, Time } from './Time';
import { IAvatarProps, Avatar } from './Avatar';
/**
* Hooks
*/
import { useHistory, useLocation } from 'react-router-dom';
import { useColorModeValue } from '@chakra-ui/react';
interface IUserComposition {
Avatar: React.FC<IAvatarProps>;
Info: React.FC<IInfoProps>;
Time: React.FC<ITimeProps>;
}
interface IUserProps {
id: string;
}
const User: React.FC<IUserProps> & IUserComposition = props => {
const history = useHistory();
const location = useLocation();
const chatId = new URLSearchParams(location.search).get('chat_id');
const bg = useColorModeValue('gray.50', 'gray.700');
const handleChat = () => {
history.replace({ pathname: location.pathname, search: new URLSearchParams({ ['chat_id']: props.id }).toString() });
};
return (
<Box
onClick={handleChat}
py='5'
px='4'
borderRadius={'lg'}
bg={chatId === props.id ? bg : ''}
_hover={{ bg }}
transition='all .1s'
cursor={'pointer'}
>
<Flex flexDir={'row'} alignItems={'center'}>
{props.children}
</Flex>
</Box>
);
};
User.Avatar = Avatar;
User.Info = Info;
User.Time = Time;
export { User };

View File

@ -0,0 +1 @@
export { User } from './User';

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Box, Stack, Text } from '@chakra-ui/react';
import { User } from './User';
import { useChatContext } from '../../context';
const tempDb = [
{ id: 'user-1', name: 'Trace Kerluke', lastMessage: 'Hello.', time: '2g' },
{ id: 'user-2', name: 'Dalton Conn ', lastMessage: 'Hello, how you doing ?', time: '30m' },
{ id: 'user-3', name: 'Cleo Hane', lastMessage: 'Hi.', time: '1h' },
];
const Users: React.FC = () => {
const { search } = useChatContext();
const filtered = !search ? tempDb : tempDb.filter(u => u.name.toLocaleLowerCase().includes(search));
const isEmpty = filtered.length === 0;
return (
<Stack mt='12' maxH='690px' overflowY={'scroll'} pr={{ base: '0', lg: '3' }}>
{!isEmpty ? (
filtered.map(u => (
<User key={u.id} id={u.id}>
<User.Avatar isOnline={false} fullname={u.name} />
<User.Info fullname={u.name} lastMessage={u.lastMessage} />
<User.Time time={u.time} />
</User>
))
) : (
<Box textAlign={'center'}>
<Text userSelect={'none'} color='gray.500'>
Cannot find any chat
</Text>
</Box>
)}
</Stack>
);
};
export { Users };

View File

@ -0,0 +1 @@
export { Users } from './Users';

View File

@ -0,0 +1 @@
export { Sidebar } from './Sidebar';

View File

@ -1,8 +0,0 @@
import React from 'react';
import { GridItem, GridItemProps } from '@chakra-ui/react';
const Window: React.FC<GridItemProps> = props => {
return <GridItem {...props}>{props.children}</GridItem>;
};
export { Window };

View File

@ -0,0 +1,16 @@
import React from 'react';
import { WrapItem, Avatar as ChakraAvatar } from '@chakra-ui/react';
export interface IAvatarProps {
fullname: string;
}
const Avatar: React.FC<IAvatarProps> = ({ fullname: name }) => {
return (
<WrapItem>
<ChakraAvatar name={name} size={'sm'} />
</WrapItem>
);
};
export { Avatar };

View File

@ -0,0 +1,29 @@
import React from 'react';
import { Flex } from '@chakra-ui/react';
import { Avatar, IAvatarProps } from './Avatar';
import { Text, ITextProps } from './Text';
import { MessageContext } from './context';
interface IMessageComposition {
Avatar: React.FC<IAvatarProps>;
Text: React.FC<ITextProps>;
}
interface IMyMessageProps {
isMyMessage: boolean;
}
const Message: React.FC<IMyMessageProps> & IMessageComposition = ({ children, isMyMessage }) => {
return (
<MessageContext.Provider value={{ isMyMessage }}>
<Flex flexDir={!isMyMessage ? 'row' : 'row-reverse'} alignItems={'center'}>
{children}
</Flex>
</MessageContext.Provider>
);
};
Message.Avatar = Avatar;
Message.Text = Text;
export { Message };

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Box, Text as ChakraText, useColorModeValue } from '@chakra-ui/react';
import { useMessageContext } from './context';
export interface ITextProps {
content: string;
time: string;
}
const Text: React.FC<ITextProps> = ({ content, time }) => {
const { isMyMessage } = useMessageContext();
const bgColor = !isMyMessage ? useColorModeValue('gray.50', 'gray.700') : useColorModeValue('gray.100', 'gray.600');
return (
<Box mx={'4'} maxW={'450px'} bg={bgColor} borderRadius='3xl' px='6' py='3'>
<ChakraText fontSize={'15px'}>
{content}
<Box
as='time'
userSelect={'none'}
position={'relative'}
left='10px'
float='right'
top='8px'
alignSelf={'flex-end'}
color='gray.500'
fontSize={'xs'}
dateTime={time}
>
{time}
</Box>
</ChakraText>
</Box>
);
};
export { Text };

View File

@ -0,0 +1,17 @@
import { createContext, useContext } from 'react';
export type MessageContextType = {
isMyMessage: boolean;
};
export const MessageContext = createContext<MessageContextType | undefined>(undefined);
export const useMessageContext = () => {
const context = useContext(MessageContext);
if (!context) {
throw new Error('MessageContext was outside of its provider');
}
return context;
};

View File

@ -0,0 +1 @@
export { Message } from './Message';

View File

@ -0,0 +1,64 @@
import React, { useEffect } from 'react';
import { GridItem, GridItemProps, Flex, Box, Icon, Text, Input, Heading, IconButton, Stack } from '@chakra-ui/react';
import { MdOutlineChat, MdSend } from 'react-icons/md';
import { Message } from './Message';
const Window: React.FC<GridItemProps> = props => {
const chatId = new URLSearchParams(location.search).get('chat_id');
useEffect(() => {
console.log('chat id changed!');
}, [chatId]);
return (
<GridItem pt={'7rem'} pb='5' px='12' {...props}>
{chatId ? (
<Flex flexDir={'column'}>
<Box my='3'>
<Heading as='h2' fontWeight={'semibold'} fontSize='3xl'>
John Doe
</Heading>
</Box>
<Stack h='65vh' overflowY={'scroll'} mb='12' mt='6' spacing={'3'} pr='8'>
<Message isMyMessage={false}>
<Message.Avatar fullname='John Doe' />
<Message.Text content={'Lorem ipsum dolor sit amet consectetur adipisicing elit.'} time='18:00' />
</Message>
<Message isMyMessage={true}>
<Message.Avatar fullname='Sergey Yarkov' />
<Message.Text content={'Lorem ipsum dolor sit amet consectetur.'} time='18:00' />
</Message>
</Stack>
<Flex mt='auto' alignItems={'flex-end'}>
<Input
resize={'block'}
type='text'
placeholder='Write a message...'
maxW='900px'
variant={'outline'}
rounded='xl'
mr='2'
/>
<IconButton
icon={<MdSend />}
aria-label='Send message'
variant={'ghost'}
_hover={{ bg: 'inherit' }}
_active={{ bg: 'inherit' }}
_focus={{ outline: 'none' }}
/>
</Flex>
</Flex>
) : (
<Box textAlign={'center'} mt={{ base: '0', lg: '15rem' }}>
<Icon as={MdOutlineChat} color='gray.600' w='16' h='16' />
<Text color='gray.500' userSelect={'none'}>
Select a chat to start messaging
</Text>
</Box>
)}
</GridItem>
);
};
export { Window };

View File

@ -0,0 +1 @@
export { Window } from './Window';

View File

@ -0,0 +1,21 @@
import React, { useContext, createContext } from 'react';
export type ChatContextType = {
search: string;
setSearch: React.Dispatch<React.SetStateAction<string>>;
};
export const ChatContext = createContext<ChatContextType>({
search: '',
setSearch: () => undefined,
});
export const useChatContext = () => {
const context = useContext(ChatContext);
if (!context) {
throw new Error('ChatContext was outside of its provider');
}
return context;
};

View File

@ -2,17 +2,21 @@ import React from 'react';
import { Grid, GridItemProps, GridProps } from '@chakra-ui/react';
import { Sidebar } from './Sidebar';
import { Window } from './Window';
import { ISidebarComposition } from './Sidebar/Sidebar';
import { ChatContextProvider } from './ChatContextProvider';
interface IChatComposition {
Sidebar: React.FC<GridItemProps>;
Sidebar: React.FC<GridItemProps> & ISidebarComposition;
Window: React.FC<GridItemProps>;
}
const Chat: React.FC<GridProps> & IChatComposition = props => {
return (
<Grid gridTemplateColumns={'1fr 3fr'} h='100vh' {...props}>
<ChatContextProvider>
<Grid gridTemplateColumns={{ base: '1fr', lg: '1fr 3fr' }} h='100vh' {...props}>
{props.children}
</Grid>
</ChatContextProvider>
);
};

View File

@ -7,7 +7,7 @@ import type { IChatButtonProps } from '.';
const ChatButton: React.FC<IChatButtonProps & ButtonProps> = props => {
const { userid } = props;
const history = useHistory();
const handleChat = () => history.push(`/chat/${userid}`);
const handleChat = () => history.push(`/messages?chat_id=${userid}`);
return (
<Button onClick={handleChat} mt='5' variant={'outline'} size={'sm'} leftIcon={<MdOutlineMessage />} {...props}>

View File

@ -1,28 +1,6 @@
import React from 'react';
import {
Avatar,
AvatarBadge,
Box,
Flex,
Heading,
WrapItem,
Text,
Stack,
Input,
Icon,
Button,
HStack,
InputGroup,
InputLeftElement,
useColorModeValue,
} from '@chakra-ui/react';
import { Link as ReactRouterLink } from 'react-router-dom';
import { Page } from '@educt/components/PageElements';
import { IPageProps } from '@educt/interfaces';
import { MdOutlineChat } from 'react-icons/md';
import { LinkBox, LinkOverlay } from '@chakra-ui/react';
import { SearchIcon } from '@chakra-ui/icons';
import { Chat } from '@educt/components/Chat';
/**
@ -34,75 +12,10 @@ const MessagesPage: React.FC<IPageProps> = () => {
<Page.Content>
<Chat>
<Chat.Sidebar>
<Flex my='4' justifyContent={'space-between'} alignItems={'center'}>
<Heading as='h2' fontSize={'md'} fontWeight='medium' mr='9'>
Chats
</Heading>
<InputGroup maxW='230px' size='sm'>
<InputLeftElement pointerEvents='none'>
<SearchIcon color='gray.300' />
</InputLeftElement>
<Input type='text' placeholder='Search' borderRadius={'md'} />
</InputGroup>
</Flex>
<HStack>
<Button isActive variant={'ghost'} size='sm'>
All
</Button>
<Button colorScheme={'blue'} variant='ghost' size='sm'>
Unread
</Button>
</HStack>
<Stack mt='12' maxH='690px' overflowY={'scroll'} pr='3'>
{Array(20)
.fill(0)
.map((e, i) => i)
.map(n => (
<LinkBox
key={n}
py='5'
px='4'
bg={n === 2 ? useColorModeValue('gray.50', 'gray.700') : ''}
borderRadius={'lg'}
_hover={{ bg: `${useColorModeValue('gray.50', 'gray.700')}` }}
transition='all .1s'
>
<Flex flexDir={'row'} alignItems={'center'}>
<WrapItem>
<Avatar name='Sergey Yarkov' size='md'>
<AvatarBadge boxSize='0.90em' bg='green.500' />
</Avatar>
</WrapItem>
<Box ml='4'>
<LinkOverlay as={ReactRouterLink} to='/'>
<Text fontWeight={'semibold'}>
Sergey Yarkov <br />
<Text as={'span'} fontWeight='normal' fontSize={'sm'} color='gray.500'>
Sergey: hello, how you doing?
</Text>
</Text>
</LinkOverlay>
</Box>
<Box alignSelf={'flex-end'} ml='auto'>
<Text as='span' color='gray.500'>
2h
</Text>
</Box>
</Flex>
</LinkBox>
))}
</Stack>
<Chat.Sidebar.Heading />
<Chat.Sidebar.Users />
</Chat.Sidebar>
<Chat.Window>
<Flex justifyContent={'center'} mt='20rem'>
<Box textAlign={'center'}>
<Icon as={MdOutlineChat} color='gray.600' w='16' h='16' />
<Text color='gray.500' userSelect={'none'}>
Select a chat to start messaging
</Text>
</Box>
</Flex>
</Chat.Window>
<Chat.Window />
</Chat>
</Page.Content>
</Page>