V 0.05
parent
c86523dcf4
commit
4d7b3dd8cd
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@ -0,0 +1,43 @@
|
|||||||
|
import { FC, useRef, useEffect } from 'react'
|
||||||
|
import TopInfo from './TopInfo';
|
||||||
|
import Message from './Message';
|
||||||
|
import EmptyChat from './EmptyChat';
|
||||||
|
import SendMessage from './SendMessage';
|
||||||
|
import { useAppSelector } from '../../../hooks/redux';
|
||||||
|
|
||||||
|
const Chat: FC = () => {
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { user } = useAppSelector(state => state.UserSlice)
|
||||||
|
const { chat } = useAppSelector(state => state.MessengerSlice)
|
||||||
|
useEffect(() => {
|
||||||
|
if(messagesEndRef.current) {
|
||||||
|
messagesEndRef.current.scrollIntoView()
|
||||||
|
}
|
||||||
|
}, [chat.messages])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-full bg-gray-300 bg-chat-pattern flex flex-col'>
|
||||||
|
<TopInfo data={chat.user}/>
|
||||||
|
<div className="overflow-y-auto flex-grow custom-scroll">
|
||||||
|
<div className={['w-full flex flex-col mx-auto max-w-[1200px]', chat.messages.length > 0 ? 'mt-10' : 'h-full justify-center'].join(' ')}>
|
||||||
|
{chat.messages.length > 0
|
||||||
|
?
|
||||||
|
<>
|
||||||
|
{chat.messages.map(message=>
|
||||||
|
<Message key={message._id} msg={message} user={message.from === user._id ? user : chat.user} isMe={message.from === user._id ? true : false}/>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef}></div>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<EmptyChat/>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<SendMessage/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chat;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import {FC} from 'react'
|
||||||
|
import { IoChatbubblesOutline } from '@react-icons/all-files/io5/IoChatbubblesOutline'
|
||||||
|
|
||||||
|
const EmptyChat: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center justify-center w-full text-gray-800'>
|
||||||
|
<div className="text-9xl mb-5"><IoChatbubblesOutline/></div>
|
||||||
|
<p className='text-xl font-medium'>Dialog is Empty</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmptyChat;
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { IMessage } from '../../../models/IMessage'
|
||||||
|
import { User } from '../../../models/User';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import { BsCheckAll } from '@react-icons/all-files/bs/BsCheckAll'
|
||||||
|
import Avatar from '../../UI/Avatar';
|
||||||
|
|
||||||
|
interface MessageProps {
|
||||||
|
msg: IMessage;
|
||||||
|
user?: User;
|
||||||
|
isMe: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Message: FC<MessageProps> = ({msg, user, isMe}) => {
|
||||||
|
return (
|
||||||
|
<div className={['flex w-1/2 mb-10', isMe ? 'self-end justify-end' : 'self-start justify-start'].join(' ')}>
|
||||||
|
<Avatar className='-translate-y-[5px] order-2' avatar={user?.avatar}/>
|
||||||
|
<div className={['shadow-md rounded-md py-2 px-4 relative before:absolute before:border-[10px] before:border-transparent before:border-t-[10px] before:top-0', isMe ? 'bg-apricot order-1 mr-5 before:border-t-apricot before:right-0 before:translate-x-1/2' : 'bg-zinc-400 order-3 ml-5 before:zinc-t-gray-400 before:left-0 before:-translate-x-1/2'].join(' ')}>
|
||||||
|
<h2 className='font-medium mb-1'>{user?.name} {user?.sname}</h2>
|
||||||
|
<p className='text-sm'>{msg.msg}</p>
|
||||||
|
<div className="flex items-center justify-end">
|
||||||
|
<p className='text-xs'>{format(msg.time, 'H:mm')}</p>
|
||||||
|
{isMe &&
|
||||||
|
<span className={['ml-1 text-lg', msg.readed ? 'text-blue-600' : 'text-gray-800'].join(' ')}><BsCheckAll/></span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Message;
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import Textarea from './Textarea';
|
||||||
|
import IcoButton from '../../UI/IcoButton';
|
||||||
|
import { BsMic } from '@react-icons/all-files/bs/BsMic';
|
||||||
|
import { VscSend } from '@react-icons/all-files/vsc/VscSend';
|
||||||
|
import {BsEmojiSmile} from '@react-icons/all-files/bs/BsEmojiSmile';
|
||||||
|
import {ImAttachment} from '@react-icons/all-files/im/ImAttachment';
|
||||||
|
|
||||||
|
const SendMessage: FC = () => {
|
||||||
|
const [msg, setMsg] = useState<string>('');
|
||||||
|
return (
|
||||||
|
<div className='bg-gray-800 border-l-2 flex items-center h-auto justify-between border-gray-700 px-12 p-2'>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<IcoButton icon={<BsEmojiSmile/>} className='!px-3 !py-2'/>
|
||||||
|
<IcoButton icon={<ImAttachment/>} className='!px-3 !py-2'/>
|
||||||
|
</div>
|
||||||
|
<Textarea msg={msg} setMsg={setMsg}/>
|
||||||
|
{msg
|
||||||
|
?
|
||||||
|
<IcoButton icon={<VscSend/>}/>
|
||||||
|
:
|
||||||
|
<IcoButton icon={<BsMic/>}/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SendMessage;
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { FC, useRef, FormEvent } from 'react'
|
||||||
|
|
||||||
|
interface TextareaProps {
|
||||||
|
msg: string;
|
||||||
|
setMsg: (msg: string) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const Textarea: FC<TextareaProps> = ({msg, setMsg}) => {
|
||||||
|
const ref = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
|
const textAreaHandler = (e: FormEvent) => {
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
setMsg(target.value);
|
||||||
|
if(ref.current) {
|
||||||
|
ref.current.style.height = "25px";
|
||||||
|
ref.current.style.height = ref.current.scrollHeight.toString()+"px";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<textarea ref={ref} value={msg} placeholder='Введите сообщение...' onInput={e=> textAreaHandler(e)} className='px-5 w-full text-sm resize-none custom-scroll focus:outline-none bg-transparent text-white h-[25px] max-h-[80px]'></textarea>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Textarea;
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { User } from '../../../models/User';
|
||||||
|
import Avatar from '../../UI/Avatar';
|
||||||
|
import IcoButton from '../../UI/IcoButton';
|
||||||
|
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
||||||
|
import { BsTelephone } from '@react-icons/all-files/bs/BsTelephone';
|
||||||
|
import { BsCameraVideo } from '@react-icons/all-files/bs/BsCameraVideo';
|
||||||
|
|
||||||
|
interface TopInfoProps {
|
||||||
|
data: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TopInfo: FC<TopInfoProps> = ({data}) => {
|
||||||
|
return (
|
||||||
|
<div className='bg-gray-800 border-l-2 flex justify-between border-gray-700 px-12 p-2'>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Avatar avatar={data.avatar} className='mr-3'/>
|
||||||
|
<div className="flex flex-col text-white">
|
||||||
|
<h2 className='font-semibold'>{data.name} {data.sname}</h2>
|
||||||
|
<p className='text-sm text-apricot'>
|
||||||
|
{data.online
|
||||||
|
?
|
||||||
|
'online'
|
||||||
|
:
|
||||||
|
formatDistanceToNow(data.lastOnline, {addSuffix: true})
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<IcoButton icon={<BsTelephone/>} className='mr-2'/>
|
||||||
|
<IcoButton icon={<BsCameraVideo/>}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopInfo;
|
||||||
@ -1,18 +1,19 @@
|
|||||||
import { FC, PropsWithChildren } from 'react'
|
import { FC, PropsWithChildren } from 'react'
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useBasePath } from '../../../hooks/useBasePath';
|
||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
to: string,
|
to: string,
|
||||||
className?: string
|
className?: string
|
||||||
|
activeOption?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const MenuItem: FC<PropsWithChildren<MenuItemProps>> = ({to, className, children}) => {
|
const MenuItem: FC<PropsWithChildren<MenuItemProps>> = ({to, className, activeOption, children}) => {
|
||||||
|
|
||||||
const location = useLocation();
|
const path = useBasePath();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={['mb-6', className].join(' ')}><Link to={to} className={['text-white hover:text-apricot transition-all text-xl font-medium', location.pathname === to ? '!text-gray-800 block bg-apricot p-4 rounded-sm' : null].join(' ')}>{children}</Link></li>
|
<li className={['mb-6', className].join(' ')}><Link to={to} className={['text-white hover:text-apricot transition-all text-xl font-medium', (path === to || path === activeOption) ? '!text-gray-800 block bg-apricot p-4 rounded-sm' : null].join(' ')}>{children}</Link></li>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { IoChatbubblesOutline } from '@react-icons/all-files/io5/IoChatbubblesOutline'
|
||||||
|
|
||||||
|
const StartMessaging: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className='flex flex-col items-center justify-center w-full text-gray-800'>
|
||||||
|
<div className="text-9xl mb-5"><IoChatbubblesOutline/></div>
|
||||||
|
<p className='text-xl font-medium'>Select chat to start messaging</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StartMessaging;
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
interface AvatarProps {
|
||||||
|
avatar?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Avatar: FC<AvatarProps> = ({avatar, className}) => {
|
||||||
|
return (
|
||||||
|
<div className={['w-12 h-12 min-w-[48px]', className].join(' ')}>
|
||||||
|
<img className='w-[inherit] h-[inherit] rounded-full' src={avatar} alt="avatar"/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Avatar;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
interface IcoButton {
|
||||||
|
icon: React.JSX.Element;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const IcoButton: FC<IcoButton> = ({icon, className}) => {
|
||||||
|
return (
|
||||||
|
<button className={['hover:bg-gray-700 py-3 px-5 rounded-sm transition-all mr-2 text-white text-xl', className].join(' ')}>{icon}</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IcoButton;
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const useBasePath = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const params = useParams<Record<string, string>>();
|
||||||
|
|
||||||
|
return Object.values(params).reduce(
|
||||||
|
(path: string, param) => path.replace(`/${String(param)}`, ''),
|
||||||
|
location.pathname,
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
export interface IMessage {
|
||||||
|
_id: string;
|
||||||
|
msg: string;
|
||||||
|
time: Date;
|
||||||
|
to?: string;
|
||||||
|
from?: string;
|
||||||
|
readed?: boolean;
|
||||||
|
}
|
||||||
@ -1,5 +0,0 @@
|
|||||||
export interface Message {
|
|
||||||
_id: string;
|
|
||||||
msg: string;
|
|
||||||
time: string;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import Chats from '../../components/Messenger/Chats/Chats';
|
||||||
|
import Chat from '../../components/Messenger/Chat/Chat';
|
||||||
|
|
||||||
|
const ChatPage: FC = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Chats/>
|
||||||
|
<Chat/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatPage;
|
||||||
@ -1,12 +1,14 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import Layout from '../../components/Layouts/Messenger';
|
|
||||||
import Chats from '../../components/Messenger/Chats/Chats';
|
import Chats from '../../components/Messenger/Chats/Chats';
|
||||||
|
import StartMessaging from '../../components/Messenger/StartMessaging';
|
||||||
|
|
||||||
const MessengerPage: FC = () => {
|
const MessengerPage: FC = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<>
|
||||||
<Chats/>
|
<Chats/>
|
||||||
</Layout>
|
<StartMessaging/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue