diff --git a/package.json b/package.json index 838e402..789e11a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/assets/Ellipse 33.png b/src/assets/Ellipse 33.png new file mode 100644 index 0000000..a7ad8dc Binary files /dev/null and b/src/assets/Ellipse 33.png differ diff --git a/src/components/chat/Chat.tsx b/src/components/chat/Chat.tsx new file mode 100644 index 0000000..e6a925c --- /dev/null +++ b/src/components/chat/Chat.tsx @@ -0,0 +1,203 @@ +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([]); + const [welcomeMessage, setWelcomeMessage] = useState< + { fistName: string; lastName: string; photo: string } | undefined + >(undefined); + const navigate = useNavigate(); + const scrollDown = useRef(null); + const { data, isError, isSuccess, isLoading } = useGetChatsQuery(userToken, { skip: !userToken }); + let socket: Socket | null = null; + + useEffect(() => { + if (userToken) { + socket = io.connect('https://e-commerce-mavericcks-bn-staging-istf.onrender.com', { auth: { token: userToken } }); + + if (isSuccess) { + setMessageList(data.chat); + } + + if (isError) { + setError('content', { message: 'Error fetching messages' }); + } + + socket.on('welcome', data => { + setWelcomeMessage(data); + }); + + socket.on('returnMessage', (msg: ChatMessage) => { + setMessageList((currentMessages: ChatMessage) => [...(currentMessages || []), msg]); + if (scrollDown.current) { + scrollDown.current.scrollIntoView({ behavior: 'smooth' }); + } + }); + + socket.on('receiveMessage', (msg: ChatMessage) => { + setMessageList(currentMessages => [...(currentMessages || []), msg]); + if (scrollDown.current) { + scrollDown.current.scrollIntoView({ behavior: 'smooth' }); + } + }); + + return () => { + socket.off('returnMessage'); + socket.off('receiveMessage'); + }; + } + }, [userToken, isSuccess, isError, data]); + + // Form validations + const { + register, + handleSubmit, + setError, + reset, + formState: { errors }, + } = useForm({ resolver: zodResolver(chatSchema) }); + + // Handlers + const handleOpenChat = () => { + userToken ? setShowChat(true) : (setShowChat(false), navigate('/login')); + }; + + const handleCloseChat = () => setShowChat(false); + + const handleMessageSubmit: SubmitHandler = async (data: ChatData) => { + const sendChatData = { ...data, socketId: String(Date.now()), senderId: userId }; + if (socket) { + await socket.emit('sentMessage', sendChatData); + } + setMessageList(currentMessage => [...(currentMessage || []), sendChatData]); + reset(); + if (scrollDown.current) scrollDown.current.scrollIntoView({ behavior: 'smooth' }); + }; + + return ( + <> +
+ +
+ + {showChat && ( +
+
+
+
+ Mavericks +
+

Mavericks Public {welcomeMessage?.fistName && {'|' + welcomeMessage.fistName}}

+ + + + + online + +
+
+
+ +
+
+
+
+ {errors.root ? ( +

{errors.root.message}

+ ) : ( + <> +

Welcome to Mavericks E-commerce website!

+

+ We’re excited to help you with exclusive services we have, let’s know how we can help you! +

+ + )} +
+
    + {isLoading ? ( +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ) : ( + messageList?.map(msg => ( +
  • + + {msg.senderId === userId ? 'Me: ' : msg.User.firstName + ': '} + {' '} + {msg.content} +
  • + )) + )} + {isError &&

    Error getting the messages!

    } +
    +
+
+
+ + + +
+
+
+ )} + + ); +}; + +export default Chat; diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index 61662ce..b156c9f 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -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 ( - <> -
-
-
- -
-

- K309 St , Makuza plaza, Nyarugenge , - Kigali, Rwanda -

-

- andela.mavericks@gmail.com -

-

+250 788888888

-
- -
-
- -
- - -
-
-
- -
- - - - - -
-
-
- -
- - - - -
-
-
- -

Be the first to get latest news about trends,Promotions and many more.

-
-
- - - -
- Email is not valid -
-
-
-

© 2024 Mavericks Shop. All rights reserved.

+ return ( + <> +
+ +
+
+ +
+

K309 St , Makuza plaza, Nyarugenge , Kigali, Rwanda

+

andela.mavericks@gmail.com

+

+250 788888888

+
+ +
+
+ +
+ + +
+
+
+ +
+ + + + + +
+
+
+ +
+ + + +
- - ) +
+
+ +

+ Be the first to get latest news about trends,Promotions and many more. +

+
+
+ + + +
+ Email is not valid +
+
+
+

+ © 2024 Mavericks Shop. All rights reserved. +

+
+ + ); } -export default Footer +export default Footer; diff --git a/src/hooks/customHooks.ts b/src/hooks/customHooks.ts new file mode 100644 index 0000000..8337b9d --- /dev/null +++ b/src/hooks/customHooks.ts @@ -0,0 +1,5 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { RootState, AppDispatch } from '../redux/store'; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/src/services/chatApi.ts b/src/services/chatApi.ts new file mode 100644 index 0000000..69b4b65 --- /dev/null +++ b/src/services/chatApi.ts @@ -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; diff --git a/src/types/Types.ts b/src/types/Types.ts index 25aeaa0..d6871ac 100644 --- a/src/types/Types.ts +++ b/src/types/Types.ts @@ -39,7 +39,7 @@ export interface CustomJwtPayload extends JwtPayload { export type Category = { id: string; name: string; - image?:string; + image?: string; }; export type CategoryResponse = { @@ -47,6 +47,7 @@ export type CategoryResponse = { message: string; data: Category[]; }; + export interface NotificationProps { id: string; message: string; @@ -54,3 +55,23 @@ export interface NotificationProps { updatedAt: string; isRead: boolean; } + +type ChatUser = { + firstName: string; + lastName: string; + photoUrl: string; +}; + +export interface ChatDataResponse { + content: string; + socketId: string; + senderId: string; + user: ChatUser; +} +export type ChatMessage = { + id: string; + content: string; + senderId: string; + socketd: string; + User: ChatUser; +}; diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 30eb47a..7335bc2 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -42,3 +42,11 @@ export const loginSchema = z.object({ }); export type LoginData = z.infer; + +// chat related schema + +export const chatSchema = z.object({ + content: z.string().min(2).max(255), +}); + +export type ChatData = z.infer;