Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

finishes [# 187354211] chat app #32

Merged
merged 1 commit into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
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.
Binary file added src/assets/chatback.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/team.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
203 changes: 203 additions & 0 deletions src/components/chat/Chat.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement>(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<ChatMessage[]>([]);

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<ChatData>({ 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<ChatData> = 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 (
<button onClick={handleNavigate} className='fixed right-5 bottom-5 text-greenColor text-4xl'>
<FaComments />
</button>
);
}

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='w-9/10 max-w-sm h-3/4 rounded-2xl shadow-xl relative bg-grayColor overflow-hidden '>
<div className='header bg-darkGreen flex justify-between items-center px-6 py-3 text-white'>
<div className='flex items-center gap-4'>
<img
src={`${profileImage !== null ? profileImage : chatAvatar}`}
alt='Mavericks'
className='w-12 h-12 rounded-full object-cover object-center'
/>
<div className='ml-4 text-whiteColor'>
<p>Mavericks Public</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 bg-[url(/assets/chatback.png)] bg-grayColor'>
<div className='introduction bg-[#767171] text-center rounded-2xl p-4 mb-6 text-[#ebe2e2]'>
{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={`p-2 relative rounded-2xl text-sm max-w-chat text-blackColor flex gap-2 items-center justify-start ${
userId === msg.senderId
? 'bg-[#95e795] text-white self-end'
: 'bg-[#ffffff] text-white self-start'
}`}
>
{/* render image as the user has one or an icon */}

<span
className={` text-greenColor flex flex-col items-center ${userId === msg.senderId ? 'hidden' : ''}`}
>
{msg.User?.photoUrl !== null ? (
<img src={msg.User?.photoUrl} alt={msg.User?.firstName} className='h-8 w-8 rounded-full' />
) : (
<FaUserCircle size={24} />
)}
<span className='text-[10px] absolute -top-4 left-2'>{msg.User?.firstName}</span>
</span>

<span>{msg.content}</span>
</li>
))
)}
{isError && <p className='tex-sm text-redColor text-center'>Error getting the messages!</p>}
<div ref={scrollDown}></div>
</ul>
</div>
<form
className='flex gap-4 p-4 border-t border-greenColor bg-grayColor relative'
onSubmit={handleSubmit(handleMessageSubmit)}
>
<textarea
{...register('content')}
className={`flex-1 p-2 border rounded-2xl outline-none md:resize-none h-9 sm:h-20 overflow-hidden ${
errors.content ? 'border-redColor' : 'border-darkGreen '
}`}
></textarea>
<button type='submit' className='text-greenColor cursor-pointer' disabled={!!errors.content}>
<FaPaperPlane size={32} className='rotate-45' />
</button>
</form>
</div>
</div>
)}
</>
);
};

export default Chat;
5 changes: 3 additions & 2 deletions src/components/footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<div className='w-full flex flex-col gap-2 bg-grayColor font-roboto 2xl:items-center'>
<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'} />
Expand Down
8 changes: 7 additions & 1 deletion src/redux/slices/userSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -35,15 +37,19 @@ const userSlice = createSlice({
setRole: (state, action: PayloadAction<string | null>) => {
state.role = action.payload;
},
setProfile: (state, action: PayloadAction<string>) => {
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;
45 changes: 45 additions & 0 deletions src/services/chatApi.ts
Original file line number Diff line number Diff line change
@@ -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<void, { content: string; senderId: string }>({
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;
20 changes: 17 additions & 3 deletions src/types/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,15 @@ export interface CustomJwtPayload extends JwtPayload {
export type Category = {
id: string;
name: string;
image?:string;
image?: string;
};

export type CategoryResponse = {
ok: boolean;
message: string;
data: Category[];
};

export interface NotificationProps {
id: string;
message: string;
Expand All @@ -66,9 +67,22 @@ export interface User {
Role: {
name: string;
};
};
}

export interface Role {
id: string;
name: string;
}
}

type ChatUser = {
firstName: string;
lastName: string;
photoUrl: string;
};

export type ChatMessage = {
id: string;
content: string;
senderId: string;
User: ChatUser;
};
7 changes: 7 additions & 0 deletions src/utils/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof chatSchema>;
Loading