Skip to content

Commit

Permalink
finishes [# 187354211] chat app
Browse files Browse the repository at this point in the history
finishes rebasing chat app
	modified:   package.json
	new file:   src/assets/Ellipse 33.png
	new file:   src/components/chat/Chat.tsx
	modified:   src/components/footer/Footer.tsx
	new file:   src/hooks/customHooks.ts
	new file:   src/services/chatApi.ts
	modified:   src/types/Types.ts
	modified:   src/utils/schemas.ts
  • Loading branch information
JeanIrad committed Jul 10, 2024
1 parent 69b2560 commit 177b9df
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 76 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react-router-dom": "^6.23.1",
"react-toastify": "^10.0.5",
"sass": "^1.77.2",
"socket.io-client": "^4.7.5",
"tailwind-merge": "^2.3.0",
"tailwind-scrollbar-hide": "^1.1.7",
"zod": "^3.23.8"
Expand Down
Binary file added src/assets/Ellipse 33.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
204 changes: 204 additions & 0 deletions src/components/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React, { useEffect, useRef, useState } from 'react';
import { FaPaperPlane, FaComments, FaTimes } from 'react-icons/fa';
import chatAvatar from '../../assets/Ellipse 33.png';
import { chatSchema, ChatData } from '../../utils/schemas';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm, SubmitHandler } from 'react-hook-form';
import { useNavigate } from 'react-router-dom';
import { useGetChatsQuery } from '../../services/chatApi';
import { useAppSelector } from '../../hooks/customHooks';
import { ChatMessage } from '../../types/Types';
import { io, Socket } from 'socket.io-client';

const Chat: React.FC = () => {
// selectors and hooks
const [showChat, setShowChat] = useState(false);
const userToken: string | undefined = useAppSelector(state => state.user.token)?.replace(/"/g, '') as string;
const userId = useAppSelector(state => state.user.userId)?.replace(/"/g, '') as string;
const [messageList, setMessageList] = useState<ChatMessage[] | undefined>([]);
const [welcomeMessage, setWelcomeMessage] = useState<
{ fistName: string; lastName: string; photo: string } | undefined
>(undefined);
const navigate = useNavigate();
const scrollDown = useRef<HTMLDivElement>(null);
const { data, isError, isSuccess, isLoading } = useGetChatsQuery(userToken, { skip: !userToken });

Check failure on line 24 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

Argument of type 'string' is not assignable to parameter of type 'unique symbol'.
let socket: Socket | null = null;
console.log(isLoading, data);

useEffect(() => {
if (userToken) {
socket = io.connect('http://localhost:8080', { auth: { token: userToken } });

Check failure on line 30 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

Property 'connect' does not exist on type '{ (opts?: Partial<ManagerOptions & SocketOptions> | undefined): Socket<DefaultEventsMap, DefaultEventsMap>; (uri: string, opts?: Partial<...> | undefined): Socket<...>; }'.

if (isSuccess) {
setMessageList(data.chat);

Check failure on line 33 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

Argument of type 'ChatDataResponse[]' is not assignable to parameter of type 'SetStateAction<ChatMessage[] | undefined>'.
}

if (isError) {
setError('content', { message: 'Error fetching messages' });
}

socket.on('welcome', data => {

Check failure on line 40 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

'socket' is possibly 'null'.
setWelcomeMessage(data);
});

socket.on('returnMessage', (msg: ChatMessage) => {

Check failure on line 44 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

'socket' is possibly 'null'.
setMessageList(currentMessages => [...(currentMessages || []), msg]);
if (scrollDown.current) {
scrollDown.current.scrollIntoView({ behavior: 'smooth' });
}
});

socket.on('receiveMessage', (msg: ChatMessage) => {

Check failure on line 51 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

'socket' is possibly 'null'.
setMessageList(currentMessages => [...(currentMessages || []), msg]);
if (scrollDown.current) {
scrollDown.current.scrollIntoView({ behavior: 'smooth' });
}
});

return () => {
socket.off('returnMessage');

Check failure on line 59 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

'socket' is possibly 'null'.
socket.off('receiveMessage');

Check failure on line 60 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

'socket' is possibly 'null'.
};
}
}, [userToken, isSuccess, isError, data]);

// Form validations
const {
register,
handleSubmit,
setError,
reset,
formState: { errors },
} = useForm<ChatData>({ resolver: zodResolver(chatSchema) });

// Handlers
const handleOpenChat = () => {
userToken ? setShowChat(true) : (setShowChat(false), navigate('/login'));
};

const handleCloseChat = () => setShowChat(false);

const handleMessageSubmit: SubmitHandler<ChatData> = async (data: ChatData) => {
const sendChatData = { ...data, socketId: String(Date.now()), senderId: userId };
if (socket) {
await socket.emit('sentMessage', sendChatData);
}
setMessageList(currentMessage => [...(currentMessage || []), sendChatData]);

Check failure on line 86 in src/components/chat/Chat.tsx

View workflow job for this annotation

GitHub Actions / Build (20)

Argument of type '(currentMessage: ChatMessage[] | undefined) => (ChatMessage | { socketId: string; senderId: string; content: string; })[]' is not assignable to parameter of type 'SetStateAction<ChatMessage[] | undefined>'.
reset();
if (scrollDown.current) scrollDown.current.scrollIntoView({ behavior: 'smooth' });
};

return (
<>
<div className='fixed bottom-5 right-5 z-50'>
<button onClick={handleOpenChat} className='text-greenColor text-4xl z-50'>
<FaComments className={showChat ? 'hidden' : ''} />
</button>
</div>

{showChat && (
<div className='fixed inset-0 z-40 flex items-center justify-end mx-2 sm:mr-6'>
<div className='bg-white w-9/10 max-w-md h-5/6 rounded-2xl shadow-xl relative bg-whiteColor'>
<div className='header bg-darkGreen flex justify-between items-center px-6 py-4 rounded-t-2xl text-white'>
<div className='flex items-center gap-4'>
<img
src={welcomeMessage === undefined ? chatAvatar : welcomeMessage.photo}
alt='Mavericks'
className='w-14 h-14 rounded-full object-cover'
/>
<div className='ml-4 text-whiteColor'>
<p>Mavericks Public {welcomeMessage?.fistName && <span>{'|' + welcomeMessage.fistName}</span>}</p>
<span className='flex items-center'>
<svg width='8' height='8' viewBox='0 0 8 8' fill='none' xmlns='http://www.w3.org/2000/svg'>
<circle cx='4' cy='4' r='4' fill='#0E9F6E' />
</svg>
<span className='ml-2'>online</span>
</span>
</div>
</div>
<div className='flex items-center'>
<button
className='text-2xl text-whiteColor transition-all hover:text-grayColor mr-2'
onClick={handleCloseChat}
>
<FaTimes size={24} />
</button>
</div>
</div>
<div className='messages-container h-2/3 p-6 flex-1 overflow-y-auto'>
<div className='introduction bg-grayColor text-center rounded-2xl p-4 mb-6'>
{errors.root ? (
<h1 className='font-semibold text-[#ff0000]'>{errors.root.message}</h1>
) : (
<>
<h1 className='font-semibold text-lg'>Welcome to Mavericks E-commerce website!</h1>
<p className='text-sm'>
We’re excited to help you with exclusive services we have, let’s know how we can help you!
</p>
</>
)}
</div>
<ul className='messages-list flex flex-col gap-4 text-whiteColor'>
{isLoading ? (
<div className='flex flex-col gap-4'>
<div className='relative flex flex-col w-full animate-pulse gap-3 p-4'>
<div className='flex-1 w-[80%]'>
<div className='h-6 rounded-lg bg-[gray] text-sm'></div>
</div>
<div className='flex-1 w-[80%] self-end'>
<div className='h-6 rounded-lg bg-[#aaa5a5] text-sm'></div>
</div>
<div className='flex-1 w-[80%]'>
<div className='h-6 rounded-lg bg-grayColor text-sm'></div>
</div>
</div>
<div className='relative flex flex-col w-full animate-pulse gap-3 p-4 self-end'>
<div className='flex-1 w-[80%] self-end'>
<div className='h-6 rounded-lg bg-[gray] text-sm'></div>
</div>
<div className='flex-1 w-[80%]'>
<div className='h-6 rounded-lg bg-[#aaa5a5] text-sm'></div>
</div>
<div className='flex-1 w-[80%] self-end'>
<div className='h-6 rounded-lg bg-grayColor text-sm'></div>
</div>
</div>
</div>
) : (
messageList?.map(msg => (
<li
key={msg.id}
className={`py-2 px-4 rounded-2xl text-sm max-w-chat ${userId === msg.senderId ? 'bg-greenColor text-white self-end' : 'bg-[#9d9494] text-white self-start'}`}
>
<span className='text-sm text-[#333354]'>
{msg.senderId === userId ? 'Me: ' : msg.User.firstName + ': '}
</span>{' '}
<span>{msg.content}</span>
</li>
))
)}
{isError && <p className='tex-sm text-redColor'>Error getting the messages!</p>}
<div ref={scrollDown}></div>
</ul>
</div>
<form
className='flex gap-4 p-4 border-t border-grayColor relative'
onSubmit={handleSubmit(handleMessageSubmit)}
>
<textarea
{...register('content')}
className={`flex-1 p-2 border rounded-2xl outline-none ${errors.content ? 'border-redColor' : 'border-darkGreen'}`}
></textarea>

<button type='submit' className='text-greenColor cursor-pointer'>
<FaPaperPlane size={32} className='rotate-45' />
</button>
</form>
</div>
</div>
)}
</>
);
};

export default Chat;
152 changes: 77 additions & 75 deletions src/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,79 +1,81 @@
import FooterTitle from "../../containers/footer/FooterTitle"
import SocialIcon from "../../containers/footer/SocialIcon"
import FooterLink from "../../containers/footer/FooterLink"

import FooterTitle from '../../containers/footer/FooterTitle';
import SocialIcon from '../../containers/footer/SocialIcon';
import FooterLink from '../../containers/footer/FooterLink';
import Chat from '../chat/Chat';
function Footer() {
return (
<>
<div className="w-full flex flex-col gap-2 bg-grayColor font-roboto 2xl:items-center ">
<div className="p-3 md:p-4 xl:px-10 2xl:w-[1440px] grid grid-cols-2 md:grid-cols-5 xl:grid-cols-6 gap-x-20 gap-y-5 sm:gap-5 md:gap-2">
<div className="flex flex-col md:row-span-1 md:col-start-1 md:col-end-3 gap-3">
<FooterTitle title={"mavericks"} />
<div className="leading-none text-xs md:text-base break-words flex flex-col gap-3 font-light flex-grow">
<p>
K309 St , Makuza plaza, Nyarugenge ,
Kigali, Rwanda
</p>
<p>
[email protected]
</p>
<p>+250 788888888</p>
</div>
<div className="flex gap-2">
<a href="#" target="_blank">
<SocialIcon name="instagram" />
</a>
<a href="#" target="_blank">
<SocialIcon name="facebook" />
</a>
<a href="#" target="_blank">
<SocialIcon name="twitter" />
</a>
</div>
</div>
<div className="flex flex-col gap-3">
<FooterTitle title={"company"} />
<div className="flex flex-col gap-1 font-light">
<FooterLink name={"about us"} />
<FooterLink name={"contact us"} />
</div>
</div>
<div className="flex flex-col gap-3">
<FooterTitle title={"shop"} />
<div className="flex flex-col gap-1 font-light">
<FooterLink name={"new arrival"} />
<FooterLink name={"all products"} />
<FooterLink name={"babies"} />
<FooterLink name={"father"} />
<FooterLink name={"electronics"} />
</div>
</div>
<div className="flex flex-col gap-3">
<FooterTitle title={"help"} />
<div className="flex flex-col gap-2 md:gap-1 font-light">
<FooterLink name={"customer services"} />
<FooterLink name={"my account"} />
<FooterLink name={"find store"} />
<FooterLink name={"legal & privacy"} />
</div>
</div>
<div className="flex flex-col items-center gap-3 row-span-1 md:row-span-1 col-span-2 md:col-start-3 md:col-end-6 lg:col-start-4 xl:col-start-6 xl:auto-cols-max">
<FooterTitle title={"subscribe"} />
<p className="hidden xl:flex leading-none text-base font-light">Be the first to get latest news about trends,Promotions and many more.</p>
<form className="w-full flex flex-col">
<div className=" w-full flex">
<label htmlFor="email"></label>
<input type="text" id="email" placeholder="Email address" className="p-2 flex-grow xl:w-3/4" />
<button type="submit" className="leading-none bg-greenColor text-whiteColor w-20">Join</button>
</div>
<span className="text-xs text-redColor text-left">Email is not valid</span>
</form>
</div>
</div>
<p className="p-3 md:p-4 xl:px-10 2xl:w-[1440px] text-xs text-center xl:text-left ">&copy; 2024 Mavericks Shop. All rights reserved.</p>
return (
<>
<div className='w-full flex flex-col gap-2 bg-grayColor font-roboto 2xl:items-center '>
<Chat />
<div className='p-3 md:p-4 xl:px-10 2xl:w-[1440px] grid grid-cols-2 md:grid-cols-5 xl:grid-cols-6 gap-x-20 gap-y-5 sm:gap-5 md:gap-2'>
<div className='flex flex-col md:row-span-1 md:col-start-1 md:col-end-3 gap-3'>
<FooterTitle title={'mavericks'} />
<div className='leading-none text-xs md:text-base break-words flex flex-col gap-3 font-light flex-grow'>
<p>K309 St , Makuza plaza, Nyarugenge , Kigali, Rwanda</p>
<p>[email protected]</p>
<p>+250 788888888</p>
</div>
<div className='flex gap-2'>
<a href='#' target='_blank'>
<SocialIcon name='instagram' />
</a>
<a href='#' target='_blank'>
<SocialIcon name='facebook' />
</a>
<a href='#' target='_blank'>
<SocialIcon name='twitter' />
</a>
</div>
</div>
<div className='flex flex-col gap-3'>
<FooterTitle title={'company'} />
<div className='flex flex-col gap-1 font-light'>
<FooterLink name={'about us'} />
<FooterLink name={'contact us'} />
</div>
</div>
<div className='flex flex-col gap-3'>
<FooterTitle title={'shop'} />
<div className='flex flex-col gap-1 font-light'>
<FooterLink name={'new arrival'} />
<FooterLink name={'all products'} />
<FooterLink name={'babies'} />
<FooterLink name={'father'} />
<FooterLink name={'electronics'} />
</div>
</div>
<div className='flex flex-col gap-3'>
<FooterTitle title={'help'} />
<div className='flex flex-col gap-2 md:gap-1 font-light'>
<FooterLink name={'customer services'} />
<FooterLink name={'my account'} />
<FooterLink name={'find store'} />
<FooterLink name={'legal & privacy'} />
</div>
</>
)
</div>
<div className='flex flex-col items-center gap-3 row-span-1 md:row-span-1 col-span-2 md:col-start-3 md:col-end-6 lg:col-start-4 xl:col-start-6 xl:auto-cols-max'>
<FooterTitle title={'subscribe'} />
<p className='hidden xl:flex leading-none text-base font-light'>
Be the first to get latest news about trends,Promotions and many more.
</p>
<form className='w-full flex flex-col'>
<div className=' w-full flex'>
<label htmlFor='email'></label>
<input type='text' id='email' placeholder='Email address' className='p-2 flex-grow xl:w-3/4' />
<button type='submit' className='leading-none bg-greenColor text-whiteColor w-20'>
Join
</button>
</div>
<span className='text-xs text-redColor text-left'>Email is not valid</span>
</form>
</div>
</div>
<p className='p-3 md:p-4 xl:px-10 2xl:w-[1440px] text-xs text-center xl:text-left '>
&copy; 2024 Mavericks Shop. All rights reserved.
</p>
</div>
</>
);
}

export default Footer
export default Footer;
5 changes: 5 additions & 0 deletions src/hooks/customHooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import { RootState, AppDispatch } from '../redux/store';

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
18 changes: 18 additions & 0 deletions src/services/chatApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { mavericksApi } from '.';
import { ChatDataResponse } from '../types/Types';

const chatApi = mavericksApi.injectEndpoints({
endpoints: builder => ({
getChats: builder.query<{ chat: ChatDataResponse[] }, undefined>({
query: token => ({
url: '/chats',
headers: {
Authorization: token,
},
}),
}),
}),
// overrideExisting: false,
});

export const { useGetChatsQuery } = chatApi;
Loading

0 comments on commit 177b9df

Please sign in to comment.