diff --git a/package.json b/package.json index 863c171..86fbc93 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,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": "^3.1.0", "tailwind-scrollbar-hide": "^1.1.7", 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/assets/chatback.png b/src/assets/chatback.png new file mode 100644 index 0000000..301d951 Binary files /dev/null and b/src/assets/chatback.png differ diff --git a/src/assets/team.jpeg b/src/assets/team.jpeg new file mode 100644 index 0000000..1c2b75e Binary files /dev/null and b/src/assets/team.jpeg differ diff --git a/src/components/chat/Chat.tsx b/src/components/chat/Chat.tsx new file mode 100644 index 0000000..96573e5 --- /dev/null +++ b/src/components/chat/Chat.tsx @@ -0,0 +1,203 @@ +import React, { useEffect, useRef, useState, useCallback } from 'react'; +import { FaPaperPlane, FaComments, FaTimes } from 'react-icons/fa'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm, SubmitHandler } from 'react-hook-form'; +import { FaUserCircle } from 'react-icons/fa'; +import { useNavigate } from 'react-router-dom'; + +import chatAvatar from '../../assets/team.jpeg'; +import { chatSchema, ChatData } from '../../utils/schemas'; +import { useGetMessagesQuery, useSendMessageMutation } from '../../services/chatApi'; +import { useAppSelector, useAppDispatch } from '../../hooks/customHooks'; +import { ChatMessage } from '../../types/Types'; +import { useGetUserByIdQuery } from '../../services/userApi'; +import { setProfile } from '../../redux/slices/userSlice'; +const Chat: React.FC = () => { + const [showChat, setShowChat] = useState(false); + const userToken = useAppSelector(state => state.user.token); + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const scrollDown = useRef(null); + // set profile image + const profileImage: string | null = useAppSelector(state => state.user.photoUrl); + const userId = useAppSelector(state => state.user.userId); + const { data, isError, isSuccess, isLoading } = useGetMessagesQuery(userToken as string, { skip: !userToken }); + const { data: userData } = useGetUserByIdQuery(userId, { skip: !userId }); + + const [messageList, setMessageList] = useState([]); + + useEffect(() => { + if (isSuccess) { + setMessageList(data?.chat); + console.log('dt', data); + if (showChat) { + scrollDown.current?.scrollIntoView({ behavior: 'smooth' }); + } + if (userData) { + console.log(userData); + dispatch(setProfile(userData.message.photoUrl)); + } + } + }, [data, isSuccess, showChat, dispatch]); + + const { + register, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ resolver: zodResolver(chatSchema) }); + + const handleOpenChat = useCallback(() => { + userToken ? setShowChat(true) : (setShowChat(false), navigate('/login')); + }, [userToken, navigate]); + + const handleCloseChat = useCallback(() => setShowChat(false), []); + + const [sendMessage] = useSendMessageMutation(); + + const handleMessageSubmit: SubmitHandler = async (data: ChatData) => { + const sendChatData = { ...data, senderId: userId as string }; + await sendMessage(sendChatData); + setMessageList((currentMessage: ChatMessage[] | any) => [...(currentMessage || []), sendChatData]); + reset(); + scrollDown.current?.scrollIntoView({ behavior: 'smooth' }); + }; + + if (!userToken) { + const handleNavigate = () => navigate('/login'); + return ( + + ); + } + + return ( + <> +
+ +
+ + {showChat && ( +
+
+
+
+ Mavericks +
+

Mavericks Public

+ + + + + 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 => ( +
  • + {/* render image as the user has one or an icon */} + + + {msg.User?.photoUrl !== null ? ( + {msg.User?.firstName} + ) : ( + + )} + {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 1b8e78b..b156c9f 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -1,11 +1,12 @@ 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 ( <> -
+
+
diff --git a/src/redux/slices/userSlice.ts b/src/redux/slices/userSlice.ts index 17a0a7d..b4c4374 100644 --- a/src/redux/slices/userSlice.ts +++ b/src/redux/slices/userSlice.ts @@ -4,12 +4,14 @@ export interface UserState { token: string | null; userId: string | null; role: string | null; + photoUrl: string | null; } const initialState: UserState = { token: localStorage.getItem('token'), userId: localStorage.getItem('user') || null, role: 'buyer', + photoUrl: null, }; const userSlice = createSlice({ @@ -35,15 +37,19 @@ const userSlice = createSlice({ setRole: (state, action: PayloadAction) => { state.role = action.payload; }, + setProfile: (state, action: PayloadAction) => { + state.photoUrl = action.payload; + }, clearUserData: state => { localStorage.removeItem('token'); localStorage.removeItem('user'); state.token = null; state.userId = null; + state.photoUrl = null; }, }, }); -export const { setToken, setUser, clearUserData, setRole } = userSlice.actions; +export const { setToken, setUser, clearUserData, setRole, setProfile } = userSlice.actions; export default userSlice.reducer; diff --git a/src/services/chatApi.ts b/src/services/chatApi.ts new file mode 100644 index 0000000..89111d1 --- /dev/null +++ b/src/services/chatApi.ts @@ -0,0 +1,45 @@ +import { mavericksApi } from '.'; +import { ChatMessage } from '../types/Types'; +import io from 'socket.io-client'; + +const token = localStorage.getItem('token'); +export const chatApi = mavericksApi.injectEndpoints({ + endpoints: builder => ({ + getMessages: builder.query<{ chat: ChatMessage[] }, string>({ + query: token => ({ + url: 'chats', + headers: { + Authorization: token, + }, + }), + async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) { + if (typeof arg !== 'undefined') { + const socket = io('https://e-commerce-mavericcks-bn-staging-istf.onrender.com', { auth: { token } }); + try { + await cacheDataLoaded; + socket.on('returnMessage', (newMessage: any) => { + updateCachedData(draft => { + draft.chat.push(newMessage); + }); + }); + } catch (err) { + console.error(err); + } + await cacheEntryRemoved; + socket.close(); + } + }, + }), + sendMessage: builder.mutation({ + queryFn: body => { + const socket = io('https://e-commerce-mavericcks-bn-staging-istf.onrender.com', { + auth: { token }, + }); + socket.emit('sentMessage', body); + return { data: undefined }; + }, + }), + }), +}); + +export const { useGetMessagesQuery, useSendMessageMutation } = chatApi; diff --git a/src/types/Types.ts b/src/types/Types.ts index ec6df7f..2a4ea2e 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; @@ -66,9 +67,22 @@ export interface User { Role: { name: string; }; -}; +} export interface Role { id: string; name: string; -} \ No newline at end of file +} + +type ChatUser = { + firstName: string; + lastName: string; + photoUrl: string; +}; + +export type ChatMessage = { + id: string; + content: string; + senderId: string; + User: ChatUser; +}; diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 7d6759f..dfe0ebf 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -131,3 +131,10 @@ export interface ApiResponse<> { message: string; data: wishListData[]; } +// chat related schema + +export const chatSchema = z.object({ + content: z.string().min(2).max(255), +}); + +export type ChatData = z.infer;