├── src
├── assets
│ └── notes.txt
├── constants
│ ├── config.js
│ ├── color.js
│ ├── events.js
│ └── sampleData.js
├── utils
│ └── validators.js
├── components
│ ├── auth
│ │ └── ProtectRoute.jsx
│ ├── shared
│ │ ├── Title.jsx
│ │ ├── RenderAttachment.jsx
│ │ ├── AvatarCard.jsx
│ │ ├── Table.jsx
│ │ ├── UserItem.jsx
│ │ ├── ChatItem.jsx
│ │ └── MessageComponent.jsx
│ ├── dialogs
│ │ ├── ConfirmDeleteDialog.jsx
│ │ ├── DeleteChatMenu.jsx
│ │ ├── AddMemberDialog.jsx
│ │ └── FileMenu.jsx
│ ├── specific
│ │ ├── ChatList.jsx
│ │ ├── Profile.jsx
│ │ ├── Charts.jsx
│ │ ├── Search.jsx
│ │ ├── Notifications.jsx
│ │ └── NewGroup.jsx
│ ├── styles
│ │ └── StyledComponents.jsx
│ └── layout
│ │ ├── Loaders.jsx
│ │ ├── AdminLayout.jsx
│ │ ├── AppLayout.jsx
│ │ └── Header.jsx
├── pages
│ ├── Home.jsx
│ ├── NotFound.jsx
│ ├── admin
│ │ ├── UserManagement.jsx
│ │ ├── AdminLogin.jsx
│ │ ├── ChatManagement.jsx
│ │ ├── MessageManagement.jsx
│ │ └── Dashboard.jsx
│ ├── Chat.jsx
│ ├── Login.jsx
│ └── Groups.jsx
├── socket.jsx
├── redux
│ ├── store.js
│ ├── thunks
│ │ └── admin.js
│ ├── reducers
│ │ ├── chat.js
│ │ ├── auth.js
│ │ └── misc.js
│ └── api
│ │ └── api.js
├── main.jsx
├── lib
│ └── features.js
├── hooks
│ └── hook.jsx
└── App.jsx
├── .env
├── vercel.json
├── vite.config.js
├── .gitignore
├── index.html
├── README.md
├── .eslintrc.cjs
├── package.json
└── public
└── vite.svg
/src/assets/notes.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 |
2 |
3 | VITE_SERVER = http://localhost:3000
--------------------------------------------------------------------------------
/src/constants/config.js:
--------------------------------------------------------------------------------
1 | export const server = import.meta.env.VITE_SERVER;
2 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
3 | }
4 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/src/utils/validators.js:
--------------------------------------------------------------------------------
1 | import { isValidUsername } from "6pp";
2 |
3 | export const usernameValidator = (username) => {
4 | if (!isValidUsername(username))
5 | return { isValid: false, errorMessage: "Username is Invalid" };
6 | };
7 |
--------------------------------------------------------------------------------
/src/components/auth/ProtectRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Navigate, Outlet } from "react-router-dom";
3 |
4 | const ProtectRoute = ({ children, user, redirect = "/login" }) => {
5 | if (!user) return ;
6 |
7 | return children ? children : ;
8 | };
9 |
10 | export default ProtectRoute;
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Chattu
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/shared/Title.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Helmet } from "react-helmet-async";
3 |
4 | const Title = ({
5 | title = "Chat App",
6 | description = "this is the Chat App called Chattu",
7 | }) => {
8 | return (
9 |
10 | {title}
11 |
12 |
13 | );
14 | };
15 |
16 | export default Title;
17 |
--------------------------------------------------------------------------------
/src/constants/color.js:
--------------------------------------------------------------------------------
1 | export const orange = "#ea7070";
2 | export const orangeLight = "rgba(234, 112, 112,0.2)";
3 |
4 | export const grayColor = "rgba(247,247,247,1)";
5 | export const lightBlue = "#2694ab";
6 | export const matBlack = "#1c1c1c";
7 | export const bgGradient = "linear-gradient(rgb(255 225 209), rgb(249 159 159))";
8 |
9 | export const purple = "rgba(75,12,192,1)";
10 | export const purpleLight = "rgba(75,12,192,0.2)";
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/src/pages/Home.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AppLayout from "../components/layout/AppLayout";
3 | import { Box, Typography } from "@mui/material";
4 | import { grayColor } from "../constants/color";
5 |
6 | const Home = () => {
7 | return (
8 |
9 |
10 | Select a friend to chat
11 |
12 |
13 | );
14 | };
15 |
16 | export default AppLayout()(Home);
17 |
--------------------------------------------------------------------------------
/src/socket.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useMemo, useContext } from "react";
2 | import io from "socket.io-client";
3 | import { server } from "./constants/config";
4 |
5 | const SocketContext = createContext();
6 |
7 | const getSocket = () => useContext(SocketContext);
8 |
9 | const SocketProvider = ({ children }) => {
10 | const socket = useMemo(() => io(server, { withCredentials: true }), []);
11 |
12 | return (
13 | {children}
14 | );
15 | };
16 |
17 | export { SocketProvider, getSocket };
18 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import authSlice from "./reducers/auth";
3 | import api from "./api/api";
4 | import miscSlice from "./reducers/misc";
5 | import chatSlice from "./reducers/chat";
6 |
7 | const store = configureStore({
8 | reducer: {
9 | [authSlice.name]: authSlice.reducer,
10 | [miscSlice.name]: miscSlice.reducer,
11 | [chatSlice.name]: chatSlice.reducer,
12 | [api.reducerPath]: api.reducer,
13 | },
14 | middleware: (mid) => [...mid(), api.middleware],
15 | });
16 |
17 | export default store;
18 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | },
21 | }
22 |
--------------------------------------------------------------------------------
/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.jsx";
4 | import { CssBaseline } from "@mui/material";
5 | import { HelmetProvider } from "react-helmet-async";
6 | import { Provider } from "react-redux";
7 | import store from "./redux/store.js";
8 |
9 | ReactDOM.createRoot(document.getElementById("root")).render(
10 |
11 |
12 |
13 |
14 | e.preventDefault()}>
15 |
16 |
17 |
18 |
19 |
20 | );
21 |
--------------------------------------------------------------------------------
/src/constants/events.js:
--------------------------------------------------------------------------------
1 | const ALERT = "ALERT";
2 | const REFETCH_CHATS = "REFETCH_CHATS";
3 |
4 | const NEW_ATTACHMENT = "NEW_ATTACHMENT";
5 | const NEW_MESSAGE_ALERT = "NEW_MESSAGE_ALERT";
6 |
7 | const NEW_REQUEST = "NEW_REQUEST";
8 | const NEW_MESSAGE = "NEW_MESSAGE";
9 |
10 | const START_TYPING = "START_TYPING";
11 | const STOP_TYPING = "STOP_TYPING";
12 |
13 | const CHAT_JOINED = "CHAT_JOINED";
14 | const CHAT_LEAVED = "CHAT_LEAVED";
15 |
16 | const ONLINE_USERS = "ONLINE_USERS";
17 |
18 | export {
19 | ALERT,
20 | REFETCH_CHATS,
21 | NEW_ATTACHMENT,
22 | NEW_MESSAGE_ALERT,
23 | NEW_REQUEST,
24 | NEW_MESSAGE,
25 | START_TYPING,
26 | STOP_TYPING,
27 | CHAT_JOINED,
28 | CHAT_LEAVED,
29 | ONLINE_USERS,
30 | };
31 |
--------------------------------------------------------------------------------
/src/pages/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Error as ErrorIcon } from "@mui/icons-material";
3 | import { Container, Stack, Typography } from "@mui/material";
4 | import { Link } from "react-router-dom";
5 |
6 | const NotFound = () => {
7 | return (
8 |
9 |
15 |
16 | 404
17 | Not Found
18 | Go back to home
19 |
20 |
21 | );
22 | };
23 |
24 | export default NotFound;
25 |
--------------------------------------------------------------------------------
/src/components/shared/RenderAttachment.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { transformImage } from "../../lib/features";
3 | import { FileOpen as FileOpenIcon } from "@mui/icons-material";
4 |
5 | const RenderAttachment = (file, url) => {
6 | switch (file) {
7 | case "video":
8 | return ;
9 |
10 | case "image":
11 | return (
12 |
21 | );
22 |
23 | case "audio":
24 | return ;
25 |
26 | default:
27 | return ;
28 | }
29 | };
30 |
31 | export default RenderAttachment;
32 |
--------------------------------------------------------------------------------
/src/components/dialogs/ConfirmDeleteDialog.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Dialog,
4 | DialogActions,
5 | DialogContent,
6 | DialogContentText,
7 | DialogTitle,
8 | } from "@mui/material";
9 | import React from "react";
10 |
11 | const ConfirmDeleteDialog = ({ open, handleClose, deleteHandler }) => {
12 | return (
13 |
27 | );
28 | };
29 |
30 | export default ConfirmDeleteDialog;
31 |
--------------------------------------------------------------------------------
/src/components/shared/AvatarCard.jsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarGroup, Box, Stack } from "@mui/material";
2 | import React from "react";
3 | import { transformImage } from "../../lib/features";
4 |
5 | // Todo Transform
6 | const AvatarCard = ({ avatar = [], max = 4 }) => {
7 | return (
8 |
9 |
15 |
16 | {avatar.map((i, index) => (
17 |
31 | ))}
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default AvatarCard;
39 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@emotion/react": "^11.11.3",
14 | "@emotion/styled": "^11.11.0",
15 | "@mui/icons-material": "^5.15.9",
16 | "@mui/material": "^5.15.9",
17 | "@mui/x-data-grid": "^6.19.4",
18 | "@reduxjs/toolkit": "^2.2.1",
19 | "6pp": "^1.1.15",
20 | "axios": "^1.6.7",
21 | "chart.js": "^4.4.1",
22 | "framer-motion": "^11.0.3",
23 | "moment": "^2.30.1",
24 | "react": "^18.2.0",
25 | "react-chartjs-2": "^5.2.0",
26 | "react-dom": "^18.2.0",
27 | "react-helmet-async": "^2.0.4",
28 | "react-hot-toast": "^2.4.1",
29 | "react-redux": "^9.1.0",
30 | "react-router-dom": "^6.22.0",
31 | "socket.io-client": "^4.7.4"
32 | },
33 | "devDependencies": {
34 | "@types/react": "^18.2.55",
35 | "@types/react-dom": "^18.2.19",
36 | "@vitejs/plugin-react-swc": "^3.5.0",
37 | "eslint": "^8.56.0",
38 | "eslint-plugin-react": "^7.33.2",
39 | "eslint-plugin-react-hooks": "^4.6.0",
40 | "eslint-plugin-react-refresh": "^0.4.5",
41 | "vite": "^5.1.0"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/components/specific/ChatList.jsx:
--------------------------------------------------------------------------------
1 | import { Stack } from "@mui/material";
2 | import React from "react";
3 | import ChatItem from "../shared/ChatItem";
4 |
5 | const ChatList = ({
6 | w = "100%",
7 | chats = [],
8 | chatId,
9 | onlineUsers = [],
10 | newMessagesAlert = [
11 | {
12 | chatId: "",
13 | count: 0,
14 | },
15 | ],
16 | handleDeleteChat,
17 | }) => {
18 | return (
19 |
20 | {chats?.map((data, index) => {
21 | const { avatar, _id, name, groupChat, members } = data;
22 |
23 | const newMessageAlert = newMessagesAlert.find(
24 | ({ chatId }) => chatId === _id
25 | );
26 |
27 | const isOnline = members?.some((member) =>
28 | onlineUsers.includes(member)
29 | );
30 |
31 | return (
32 |
44 | );
45 | })}
46 |
47 | );
48 | };
49 |
50 | export default ChatList;
51 |
--------------------------------------------------------------------------------
/src/redux/thunks/admin.js:
--------------------------------------------------------------------------------
1 | import { createAsyncThunk } from "@reduxjs/toolkit";
2 | import { server } from "../../constants/config";
3 | import axios from "axios";
4 |
5 | const adminLogin = createAsyncThunk("admin/login", async (secretKey) => {
6 | try {
7 | const config = {
8 | withCredentials: true,
9 | headers: {
10 | "Content-Type": "application/json",
11 | },
12 | };
13 |
14 | const { data } = await axios.post(
15 | `${server}/api/v1/admin/verify`,
16 | { secretKey },
17 | config
18 | );
19 |
20 | return data.message;
21 | } catch (error) {
22 | throw error.response.data.message;
23 | }
24 | });
25 |
26 | const getAdmin = createAsyncThunk("admin/getAdmin", async () => {
27 | try {
28 | const { data } = await axios.get(`${server}/api/v1/admin/`, {
29 | withCredentials: true,
30 | });
31 |
32 | return data.admin;
33 | } catch (error) {
34 | throw error.response.data.message;
35 | }
36 | });
37 |
38 | const adminLogout = createAsyncThunk("admin/logout", async () => {
39 | try {
40 | const { data } = await axios.get(`${server}/api/v1/admin/logout`, {
41 | withCredentials: true,
42 | });
43 |
44 | return data.message;
45 | } catch (error) {
46 | throw error.response.data.message;
47 | }
48 | });
49 |
50 | export { adminLogin, getAdmin, adminLogout };
51 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/shared/Table.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { DataGrid } from "@mui/x-data-grid";
3 | import { Container, Paper, Typography } from "@mui/material";
4 | import { matBlack } from "../../constants/color";
5 |
6 | const Table = ({ rows, columns, heading, rowHeight = 52 }) => {
7 | return (
8 |
13 |
25 |
33 | {heading}
34 |
35 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default Table;
56 |
--------------------------------------------------------------------------------
/src/lib/features.js:
--------------------------------------------------------------------------------
1 | import moment from "moment";
2 |
3 | const fileFormat = (url = "") => {
4 | const fileExt = url.split(".").pop();
5 |
6 | if (fileExt === "mp4" || fileExt === "webm" || fileExt === "ogg")
7 | return "video";
8 |
9 | if (fileExt === "mp3" || fileExt === "wav") return "audio";
10 | if (
11 | fileExt === "png" ||
12 | fileExt === "jpg" ||
13 | fileExt === "jpeg" ||
14 | fileExt === "gif"
15 | )
16 | return "image";
17 |
18 | return "file";
19 | };
20 |
21 | // https://res.cloudinary.com/dj5q966nb/image/upload/dpr_auto/w_200/v1710344436/fafceddc-2845-4ae7-a25a-632f01922b4d.png
22 |
23 | // /dpr_auto/w_200
24 | const transformImage = (url = "", width = 100) => {
25 | const newUrl = url.replace("upload/", `upload/dpr_auto/w_${width}/`);
26 |
27 | return newUrl;
28 | };
29 |
30 | const getLast7Days = () => {
31 | const currentDate = moment();
32 |
33 | const last7Days = [];
34 |
35 | for (let i = 0; i < 7; i++) {
36 | const dayDate = currentDate.clone().subtract(i, "days");
37 | const dayName = dayDate.format("dddd");
38 |
39 | last7Days.unshift(dayName);
40 | }
41 |
42 | return last7Days;
43 | };
44 |
45 | const getOrSaveFromStorage = ({ key, value, get }) => {
46 | if (get)
47 | return localStorage.getItem(key)
48 | ? JSON.parse(localStorage.getItem(key))
49 | : null;
50 | else localStorage.setItem(key, JSON.stringify(value));
51 | };
52 |
53 | export { fileFormat, transformImage, getLast7Days, getOrSaveFromStorage };
54 |
--------------------------------------------------------------------------------
/src/redux/reducers/chat.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { getOrSaveFromStorage } from "../../lib/features";
3 | import { NEW_MESSAGE_ALERT } from "../../constants/events";
4 |
5 | const initialState = {
6 | notificationCount: 0,
7 | newMessagesAlert: getOrSaveFromStorage({
8 | key: NEW_MESSAGE_ALERT,
9 | get: true,
10 | }) || [
11 | {
12 | chatId: "",
13 | count: 0,
14 | },
15 | ],
16 | };
17 |
18 | const chatSlice = createSlice({
19 | name: "chat",
20 | initialState,
21 | reducers: {
22 | incrementNotification: (state) => {
23 | state.notificationCount += 1;
24 | },
25 | resetNotificationCount: (state) => {
26 | state.notificationCount = 0;
27 | },
28 |
29 | setNewMessagesAlert: (state, action) => {
30 | const chatId = action.payload.chatId;
31 |
32 | const index = state.newMessagesAlert.findIndex(
33 | (item) => item.chatId === chatId
34 | );
35 |
36 | if (index !== -1) {
37 | state.newMessagesAlert[index].count += 1;
38 | } else {
39 | state.newMessagesAlert.push({
40 | chatId,
41 | count: 1,
42 | });
43 | }
44 | },
45 |
46 | removeNewMessagesAlert: (state, action) => {
47 | state.newMessagesAlert = state.newMessagesAlert.filter(
48 | (item) => item.chatId !== action.payload
49 | );
50 | },
51 | },
52 | });
53 |
54 | export default chatSlice;
55 | export const {
56 | incrementNotification,
57 | resetNotificationCount,
58 | setNewMessagesAlert,
59 | removeNewMessagesAlert,
60 | } = chatSlice.actions;
61 |
--------------------------------------------------------------------------------
/src/components/shared/UserItem.jsx:
--------------------------------------------------------------------------------
1 | import { Add as AddIcon, Remove as RemoveIcon } from "@mui/icons-material";
2 | import { Avatar, IconButton, ListItem, Stack, Typography } from "@mui/material";
3 | import React, { memo } from "react";
4 | import { transformImage } from "../../lib/features";
5 |
6 | const UserItem = ({
7 | user,
8 | handler,
9 | handlerIsLoading,
10 | isAdded = false,
11 | styling = {},
12 | }) => {
13 | const { name, _id, avatar } = user;
14 |
15 | return (
16 |
17 |
24 |
25 |
26 |
38 | {name}
39 |
40 |
41 | handler(_id)}
51 | disabled={handlerIsLoading}
52 | >
53 | {isAdded ? : }
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default memo(UserItem);
61 |
--------------------------------------------------------------------------------
/src/components/specific/Profile.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Avatar, Stack, Typography } from "@mui/material";
3 | import {
4 | Face as FaceIcon,
5 | AlternateEmail as UserNameIcon,
6 | CalendarMonth as CalendarIcon,
7 | } from "@mui/icons-material";
8 | import moment from "moment";
9 | import { transformImage } from "../../lib/features";
10 |
11 | const Profile = ({ user }) => {
12 | return (
13 |
14 |
24 |
25 | }
29 | />
30 | } />
31 | }
35 | />
36 |
37 | );
38 | };
39 |
40 | const ProfileCard = ({ text, Icon, heading }) => (
41 |
48 | {Icon && Icon}
49 |
50 |
51 | {text}
52 |
53 | {heading}
54 |
55 |
56 |
57 | );
58 |
59 | export default Profile;
60 |
--------------------------------------------------------------------------------
/src/redux/reducers/auth.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { adminLogin, adminLogout, getAdmin } from "../thunks/admin";
3 | import toast from "react-hot-toast";
4 |
5 | const initialState = {
6 | user: null,
7 | isAdmin: false,
8 | loader: true,
9 | };
10 |
11 | const authSlice = createSlice({
12 | name: "auth",
13 | initialState,
14 | reducers: {
15 | userExists: (state, action) => {
16 | state.user = action.payload;
17 | state.loader = false;
18 | },
19 | userNotExists: (state) => {
20 | state.user = null;
21 | state.loader = false;
22 | },
23 | },
24 |
25 | extraReducers: (builder) => {
26 | builder
27 | .addCase(adminLogin.fulfilled, (state, action) => {
28 | state.isAdmin = true;
29 | toast.success(action.payload);
30 | })
31 | .addCase(adminLogin.rejected, (state, action) => {
32 | state.isAdmin = false;
33 | toast.error(action.error.message);
34 | })
35 | .addCase(getAdmin.fulfilled, (state, action) => {
36 | if (action.payload) {
37 | state.isAdmin = true;
38 | } else {
39 | state.isAdmin = false;
40 | }
41 | })
42 | .addCase(getAdmin.rejected, (state, action) => {
43 | state.isAdmin = false;
44 | })
45 | .addCase(adminLogout.fulfilled, (state, action) => {
46 | state.isAdmin = false;
47 | toast.success(action.payload);
48 | })
49 | .addCase(adminLogout.rejected, (state, action) => {
50 | state.isAdmin = true;
51 | toast.error(action.error.message);
52 | });
53 | },
54 | });
55 |
56 | export default authSlice;
57 | export const { userExists, userNotExists } = authSlice.actions;
58 |
--------------------------------------------------------------------------------
/src/redux/reducers/misc.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | isNewGroup: false,
5 | isAddMember: false,
6 | isNotification: false,
7 | isMobile: false,
8 | isSearch: false,
9 | isFileMenu: false,
10 | isDeleteMenu: false,
11 | uploadingLoader: false,
12 | selectedDeleteChat: {
13 | chatId: "",
14 | groupChat: false,
15 | },
16 | };
17 |
18 | const miscSlice = createSlice({
19 | name: "misc",
20 | initialState,
21 | reducers: {
22 | setIsNewGroup: (state, action) => {
23 | state.isNewGroup = action.payload;
24 | },
25 | setIsAddMember: (state, action) => {
26 | state.isAddMember = action.payload;
27 | },
28 | setIsNotification: (state, action) => {
29 | state.isNotification = action.payload;
30 | },
31 | setIsMobile: (state, action) => {
32 | state.isMobile = action.payload;
33 | },
34 | setIsSearch: (state, action) => {
35 | state.isSearch = action.payload;
36 | },
37 | setIsFileMenu: (state, action) => {
38 | state.isFileMenu = action.payload;
39 | },
40 | setIsDeleteMenu: (state, action) => {
41 | state.isDeleteMenu = action.payload;
42 | },
43 | setUploadingLoader: (state, action) => {
44 | state.uploadingLoader = action.payload;
45 | },
46 | setSelectedDeleteChat: (state, action) => {
47 | state.selectedDeleteChat = action.payload;
48 | },
49 | },
50 | });
51 |
52 | export default miscSlice;
53 | export const {
54 | setIsNewGroup,
55 | setIsAddMember,
56 | setIsNotification,
57 | setIsMobile,
58 | setIsSearch,
59 | setIsFileMenu,
60 | setIsDeleteMenu,
61 | setUploadingLoader,
62 | setSelectedDeleteChat,
63 | } = miscSlice.actions;
64 |
--------------------------------------------------------------------------------
/src/components/styles/StyledComponents.jsx:
--------------------------------------------------------------------------------
1 | import { Skeleton, keyframes, styled } from "@mui/material";
2 | import { Link as LinkComponent } from "react-router-dom";
3 | import { grayColor, matBlack } from "../../constants/color";
4 |
5 | const VisuallyHiddenInput = styled("input")({
6 | border: 0,
7 | clip: "rect(0 0 0 0)",
8 | height: 1,
9 | margin: -1,
10 | overflow: "hidden",
11 | padding: 0,
12 | position: "absolute",
13 | whiteSpace: "nowrap",
14 | width: 1,
15 | });
16 |
17 | const Link = styled(LinkComponent)`
18 | text-decoration: none;
19 | color: black;
20 | padding: 1rem;
21 | &:hover {
22 | background-color: rgba(0, 0, 0, 0.1);
23 | }
24 | `;
25 |
26 | const InputBox = styled("input")`
27 | width: 100%;
28 | height: 100%;
29 | border: none;
30 | outline: none;
31 | padding: 0 3rem;
32 | border-radius: 1.5rem;
33 | background-color: ${grayColor};
34 | `;
35 |
36 | const SearchField = styled("input")`
37 | padding: 1rem 2rem;
38 | width: 20vmax;
39 | border: none;
40 | outline: none;
41 | border-radius: 1.5rem;
42 | background-color: ${grayColor};
43 | font-size: 1.1rem;
44 | `;
45 |
46 | const CurveButton = styled("button")`
47 | border-radius: 1.5rem;
48 | padding: 1rem 2rem;
49 | border: none;
50 | outline: none;
51 | cursor: pointer;
52 | background-color: ${matBlack};
53 | color: white;
54 | font-size: 1.1rem;
55 | &:hover {
56 | background-color: rgba(0, 0, 0, 0.8);
57 | }
58 | `;
59 |
60 | const bounceAnimation = keyframes`
61 | 0% { transform: scale(1); }
62 | 50% { transform: scale(1.5); }
63 | 100% { transform: scale(1); }
64 | `;
65 |
66 | const BouncingSkeleton = styled(Skeleton)(() => ({
67 | animation: `${bounceAnimation} 1s infinite`,
68 | }));
69 |
70 | export {
71 | CurveButton,
72 | SearchField,
73 | InputBox,
74 | Link,
75 | VisuallyHiddenInput,
76 | BouncingSkeleton,
77 | };
78 |
--------------------------------------------------------------------------------
/src/hooks/hook.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import toast from "react-hot-toast";
3 |
4 | const useErrors = (errors = []) => {
5 | useEffect(() => {
6 | errors.forEach(({ isError, error, fallback }) => {
7 | if (isError) {
8 | if (fallback) fallback();
9 | else toast.error(error?.data?.message || "Something went wrong");
10 | }
11 | });
12 | }, [errors]);
13 | };
14 |
15 | const useAsyncMutation = (mutatationHook) => {
16 | const [isLoading, setIsLoading] = useState(false);
17 | const [data, setData] = useState(null);
18 |
19 | const [mutate] = mutatationHook();
20 |
21 | const executeMutation = async (toastMessage, ...args) => {
22 | setIsLoading(true);
23 | const toastId = toast.loading(toastMessage || "Updating data...");
24 |
25 | try {
26 | const res = await mutate(...args);
27 |
28 | if (res.data) {
29 | toast.success(res.data.message || "Updated data successfully", {
30 | id: toastId,
31 | });
32 | setData(res.data);
33 | } else {
34 | toast.error(res?.error?.data?.message || "Something went wrong", {
35 | id: toastId,
36 | });
37 | }
38 | } catch (error) {
39 | console.log(error);
40 | toast.error("Something went wrong", { id: toastId });
41 | } finally {
42 | setIsLoading(false);
43 | }
44 | };
45 |
46 | return [executeMutation, isLoading, data];
47 | };
48 |
49 | const useSocketEvents = (socket, handlers) => {
50 | useEffect(() => {
51 | Object.entries(handlers).forEach(([event, handler]) => {
52 | socket.on(event, handler);
53 | });
54 |
55 | return () => {
56 | Object.entries(handlers).forEach(([event, handler]) => {
57 | socket.off(event, handler);
58 | });
59 | };
60 | }, [socket, handlers]);
61 | };
62 |
63 | export { useErrors, useAsyncMutation, useSocketEvents };
64 |
--------------------------------------------------------------------------------
/src/components/shared/ChatItem.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import { Link } from "../styles/StyledComponents";
3 | import { Box, Stack, Typography } from "@mui/material";
4 | import AvatarCard from "./AvatarCard";
5 | import { motion } from "framer-motion";
6 |
7 | const ChatItem = ({
8 | avatar = [],
9 | name,
10 | _id,
11 | groupChat = false,
12 | sameSender,
13 | isOnline,
14 | newMessageAlert,
15 | index = 0,
16 | handleDeleteChat,
17 | }) => {
18 | return (
19 | handleDeleteChat(e, _id, groupChat)}
25 | >
26 |
40 |
41 |
42 |
43 | {name}
44 | {newMessageAlert && (
45 | {newMessageAlert.count} New Message
46 | )}
47 |
48 |
49 | {isOnline && (
50 |
62 | )}
63 |
64 |
65 | );
66 | };
67 |
68 | export default memo(ChatItem);
69 |
--------------------------------------------------------------------------------
/src/components/shared/MessageComponent.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from "@mui/material";
2 | import React, { memo } from "react";
3 | import { lightBlue } from "../../constants/color";
4 | import moment from "moment";
5 | import { fileFormat } from "../../lib/features";
6 | import RenderAttachment from "./RenderAttachment";
7 | import { motion } from "framer-motion";
8 |
9 | const MessageComponent = ({ message, user }) => {
10 | const { sender, content, attachments = [], createdAt } = message;
11 |
12 | const sameSender = sender?._id === user?._id;
13 |
14 | const timeAgo = moment(createdAt).fromNow();
15 |
16 | return (
17 |
29 | {!sameSender && (
30 |
31 | {sender.name}
32 |
33 | )}
34 |
35 | {content && {content}}
36 |
37 | {attachments.length > 0 &&
38 | attachments.map((attachment, index) => {
39 | const url = attachment.url;
40 | const file = fileFormat(url);
41 |
42 | return (
43 |
44 |
52 | {RenderAttachment(file, url)}
53 |
54 |
55 | );
56 | })}
57 |
58 |
59 | {timeAgo}
60 |
61 |
62 | );
63 | };
64 |
65 | export default memo(MessageComponent);
66 |
--------------------------------------------------------------------------------
/src/components/layout/Loaders.jsx:
--------------------------------------------------------------------------------
1 | import { Grid, Skeleton, Stack } from "@mui/material";
2 | import React from "react";
3 | import { BouncingSkeleton } from "../styles/StyledComponents";
4 |
5 | const LayoutLoader = () => {
6 | return (
7 |
8 |
17 |
18 |
19 |
20 |
21 | {Array.from({ length: 10 }).map((_, index) => (
22 |
23 | ))}
24 |
25 |
26 |
27 |
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | const TypingLoader = () => {
43 | return (
44 |
50 |
58 |
66 |
74 |
82 |
83 | );
84 | };
85 |
86 | export { TypingLoader, LayoutLoader };
87 |
--------------------------------------------------------------------------------
/src/pages/admin/UserManagement.jsx:
--------------------------------------------------------------------------------
1 | import { useFetchData } from "6pp";
2 | import { Avatar, Skeleton } from "@mui/material";
3 | import React, { useEffect, useState } from "react";
4 | import AdminLayout from "../../components/layout/AdminLayout";
5 | import Table from "../../components/shared/Table";
6 | import { server } from "../../constants/config";
7 | import { useErrors } from "../../hooks/hook";
8 | import { transformImage } from "../../lib/features";
9 |
10 | const columns = [
11 | {
12 | field: "id",
13 | headerName: "ID",
14 | headerClassName: "table-header",
15 | width: 200,
16 | },
17 | {
18 | field: "avatar",
19 | headerName: "Avatar",
20 | headerClassName: "table-header",
21 | width: 150,
22 | renderCell: (params) => (
23 |
24 | ),
25 | },
26 |
27 | {
28 | field: "name",
29 | headerName: "Name",
30 | headerClassName: "table-header",
31 | width: 200,
32 | },
33 | {
34 | field: "username",
35 | headerName: "Username",
36 | headerClassName: "table-header",
37 | width: 200,
38 | },
39 | {
40 | field: "friends",
41 | headerName: "Friends",
42 | headerClassName: "table-header",
43 | width: 150,
44 | },
45 | {
46 | field: "groups",
47 | headerName: "Groups",
48 | headerClassName: "table-header",
49 | width: 200,
50 | },
51 | ];
52 | const UserManagement = () => {
53 | const { loading, data, error } = useFetchData(
54 | `${server}/api/v1/admin/users`,
55 | "dashboard-users"
56 | );
57 |
58 | useErrors([
59 | {
60 | isError: error,
61 | error: error,
62 | },
63 | ]);
64 |
65 | const [rows, setRows] = useState([]);
66 |
67 | useEffect(() => {
68 | if (data) {
69 | setRows(
70 | data.users.map((i) => ({
71 | ...i,
72 | id: i._id,
73 | avatar: transformImage(i.avatar, 50),
74 | }))
75 | );
76 | }
77 | }, [data]);
78 |
79 | return (
80 |
81 | {loading ? (
82 |
83 | ) : (
84 |
85 | )}
86 |
87 | );
88 | };
89 |
90 | export default UserManagement;
91 |
--------------------------------------------------------------------------------
/src/components/specific/Charts.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | ArcElement,
3 | CategoryScale,
4 | Chart as ChartJS,
5 | Filler,
6 | Legend,
7 | LineElement,
8 | LinearScale,
9 | PointElement,
10 | Tooltip,
11 | } from "chart.js";
12 | import React from "react";
13 | import { Doughnut, Line } from "react-chartjs-2";
14 | import {
15 | orange,
16 | orangeLight,
17 | purple,
18 | purpleLight,
19 | } from "../../constants/color";
20 | import { getLast7Days } from "../../lib/features";
21 |
22 | ChartJS.register(
23 | Tooltip,
24 | CategoryScale,
25 | LinearScale,
26 | LineElement,
27 | PointElement,
28 | Filler,
29 | ArcElement,
30 | Legend
31 | );
32 |
33 | const labels = getLast7Days();
34 |
35 | const lineChartOptions = {
36 | responsive: true,
37 | plugins: {
38 | legend: {
39 | display: false,
40 | },
41 | title: {
42 | display: false,
43 | },
44 | },
45 |
46 | scales: {
47 | x: {
48 | grid: {
49 | display: false,
50 | },
51 | },
52 | y: {
53 | beginAtZero: true,
54 | grid: {
55 | display: false,
56 | },
57 | },
58 | },
59 | };
60 |
61 | const LineChart = ({ value = [] }) => {
62 | const data = {
63 | labels,
64 | datasets: [
65 | {
66 | data: value,
67 | label: "Messages",
68 | fill: true,
69 | backgroundColor: purpleLight,
70 | borderColor: purple,
71 | },
72 | ],
73 | };
74 |
75 | return ;
76 | };
77 |
78 | const doughnutChartOptions = {
79 | responsive: true,
80 | plugins: {
81 | legend: {
82 | display: false,
83 | },
84 | },
85 | cutout: 120,
86 | };
87 |
88 | const DoughnutChart = ({ value = [], labels = [] }) => {
89 | const data = {
90 | labels,
91 | datasets: [
92 | {
93 | data: value,
94 | backgroundColor: [purpleLight, orangeLight],
95 | hoverBackgroundColor: [purple, orange],
96 | borderColor: [purple, orange],
97 | offset: 40,
98 | },
99 | ],
100 | };
101 | return (
102 |
107 | );
108 | };
109 |
110 | export { DoughnutChart, LineChart };
111 |
--------------------------------------------------------------------------------
/src/pages/admin/AdminLogin.jsx:
--------------------------------------------------------------------------------
1 | import { useInputValidation } from "6pp";
2 | import {
3 | Button,
4 | Container,
5 | Paper,
6 | TextField,
7 | Typography
8 | } from "@mui/material";
9 | import React, { useEffect } from "react";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import { Navigate } from "react-router-dom";
12 | import { bgGradient } from "../../constants/color";
13 | import { adminLogin, getAdmin } from "../../redux/thunks/admin";
14 |
15 | const AdminLogin = () => {
16 | const { isAdmin } = useSelector((state) => state.auth);
17 |
18 | const dispatch = useDispatch();
19 |
20 | const secretKey = useInputValidation("");
21 |
22 | const submitHandler = (e) => {
23 | e.preventDefault();
24 | dispatch(adminLogin(secretKey.value));
25 | };
26 |
27 | useEffect(() => {
28 | dispatch(getAdmin());
29 | }, [dispatch]);
30 |
31 | if (isAdmin) return ;
32 |
33 | return (
34 |
39 |
49 |
58 | Admin Login
59 |
89 |
90 |
91 |
92 | );
93 | };
94 |
95 | export default AdminLogin;
96 |
--------------------------------------------------------------------------------
/src/components/specific/Search.jsx:
--------------------------------------------------------------------------------
1 | import { useInputValidation } from "6pp";
2 | import { Search as SearchIcon } from "@mui/icons-material";
3 | import {
4 | Dialog,
5 | DialogTitle,
6 | InputAdornment,
7 | List,
8 | Stack,
9 | TextField,
10 | } from "@mui/material";
11 | import React, { useEffect, useState } from "react";
12 | import { useDispatch, useSelector } from "react-redux";
13 | import { useAsyncMutation } from "../../hooks/hook";
14 | import {
15 | useLazySearchUserQuery,
16 | useSendFriendRequestMutation,
17 | } from "../../redux/api/api";
18 | import { setIsSearch } from "../../redux/reducers/misc";
19 | import UserItem from "../shared/UserItem";
20 |
21 | const Search = () => {
22 | const { isSearch } = useSelector((state) => state.misc);
23 |
24 | const [searchUser] = useLazySearchUserQuery();
25 |
26 | const [sendFriendRequest, isLoadingSendFriendRequest] = useAsyncMutation(
27 | useSendFriendRequestMutation
28 | );
29 |
30 | const dispatch = useDispatch();
31 |
32 | const search = useInputValidation("");
33 |
34 | const [users, setUsers] = useState([]);
35 |
36 | const addFriendHandler = async (id) => {
37 | await sendFriendRequest("Sending friend request...", { userId: id });
38 | };
39 |
40 | const searchCloseHandler = () => dispatch(setIsSearch(false));
41 |
42 | useEffect(() => {
43 | const timeOutId = setTimeout(() => {
44 | searchUser(search.value)
45 | .then(({ data }) => setUsers(data.users))
46 | .catch((e) => console.log(e));
47 | }, 1000);
48 |
49 | return () => {
50 | clearTimeout(timeOutId);
51 | };
52 | }, [search.value]);
53 |
54 | return (
55 |
85 | );
86 | };
87 |
88 | export default Search;
89 |
--------------------------------------------------------------------------------
/src/components/dialogs/DeleteChatMenu.jsx:
--------------------------------------------------------------------------------
1 | import { Menu, Stack, Typography } from "@mui/material";
2 | import React, { useEffect } from "react";
3 | import { useSelector } from "react-redux";
4 | import { setIsDeleteMenu } from "../../redux/reducers/misc";
5 | import {
6 | Delete as DeleteIcon,
7 | ExitToApp as ExitToAppIcon,
8 | } from "@mui/icons-material";
9 | import { useNavigate } from "react-router-dom";
10 | import { useAsyncMutation } from "../../hooks/hook";
11 | import {
12 | useDeleteChatMutation,
13 | useLeaveGroupMutation,
14 | } from "../../redux/api/api";
15 |
16 | const DeleteChatMenu = ({ dispatch, deleteMenuAnchor }) => {
17 | const navigate = useNavigate();
18 |
19 | const { isDeleteMenu, selectedDeleteChat } = useSelector(
20 | (state) => state.misc
21 | );
22 |
23 | const [deleteChat, _, deleteChatData] = useAsyncMutation(
24 | useDeleteChatMutation
25 | );
26 |
27 | const [leaveGroup, __, leaveGroupData] = useAsyncMutation(
28 | useLeaveGroupMutation
29 | );
30 |
31 | const isGroup = selectedDeleteChat.groupChat;
32 |
33 | const closeHandler = () => {
34 | dispatch(setIsDeleteMenu(false));
35 | deleteMenuAnchor.current = null;
36 | };
37 |
38 | const leaveGroupHandler = () => {
39 | closeHandler();
40 | leaveGroup("Leaving Group...", selectedDeleteChat.chatId);
41 | };
42 |
43 | const deleteChatHandler = () => {
44 | closeHandler();
45 | deleteChat("Deleting Chat...", selectedDeleteChat.chatId);
46 | };
47 |
48 | useEffect(() => {
49 | if (deleteChatData || leaveGroupData) navigate("/");
50 | }, [deleteChatData, leaveGroupData]);
51 |
52 | return (
53 |
90 | );
91 | };
92 |
93 | export default DeleteChatMenu;
94 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, lazy, useEffect } from "react";
2 | import { BrowserRouter, Routes, Route } from "react-router-dom";
3 | import ProtectRoute from "./components/auth/ProtectRoute";
4 | import { LayoutLoader } from "./components/layout/Loaders";
5 | import axios from "axios";
6 | import { server } from "./constants/config";
7 | import { useDispatch, useSelector } from "react-redux";
8 | import { userExists, userNotExists } from "./redux/reducers/auth";
9 | import { Toaster } from "react-hot-toast";
10 | import { SocketProvider } from "./socket";
11 |
12 | const Home = lazy(() => import("./pages/Home"));
13 | const Login = lazy(() => import("./pages/Login"));
14 | const Chat = lazy(() => import("./pages/Chat"));
15 | const Groups = lazy(() => import("./pages/Groups"));
16 | const NotFound = lazy(() => import("./pages/NotFound"));
17 |
18 | const AdminLogin = lazy(() => import("./pages/admin/AdminLogin"));
19 | const Dashboard = lazy(() => import("./pages/admin/Dashboard"));
20 | const UserManagement = lazy(() => import("./pages/admin/UserManagement"));
21 | const ChatManagement = lazy(() => import("./pages/admin/ChatManagement"));
22 | const MessagesManagement = lazy(() =>
23 | import("./pages/admin/MessageManagement")
24 | );
25 |
26 | const App = () => {
27 | const { user, loader } = useSelector((state) => state.auth);
28 |
29 | const dispatch = useDispatch();
30 |
31 | useEffect(() => {
32 | axios
33 | .get(`${server}/api/v1/user/me`, { withCredentials: true })
34 | .then(({ data }) => dispatch(userExists(data.user)))
35 | .catch((err) => dispatch(userNotExists()));
36 | }, [dispatch]);
37 |
38 | return loader ? (
39 |
40 | ) : (
41 |
42 | }>
43 |
44 |
47 |
48 |
49 | }
50 | >
51 | } />
52 | } />
53 | } />
54 |
55 |
56 |
60 |
61 |
62 | }
63 | />
64 |
65 | } />
66 | } />
67 | } />
68 | } />
69 | } />
70 |
71 | } />
72 |
73 |
74 |
75 |
76 |
77 | );
78 | };
79 |
80 | export default App;
81 |
--------------------------------------------------------------------------------
/src/components/dialogs/AddMemberDialog.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Dialog,
4 | DialogTitle,
5 | Skeleton,
6 | Stack,
7 | Typography,
8 | } from "@mui/material";
9 | import React, { useState } from "react";
10 | import { sampleUsers } from "../../constants/sampleData";
11 | import UserItem from "../shared/UserItem";
12 | import {
13 | useAddGroupMembersMutation,
14 | useAvailableFriendsQuery,
15 | } from "../../redux/api/api";
16 | import { useAsyncMutation, useErrors } from "../../hooks/hook";
17 | import { useDispatch, useSelector } from "react-redux";
18 | import { setIsAddMember } from "../../redux/reducers/misc";
19 | const AddMemberDialog = ({ chatId }) => {
20 | const dispatch = useDispatch();
21 |
22 | const { isAddMember } = useSelector((state) => state.misc);
23 |
24 | const { isLoading, data, isError, error } = useAvailableFriendsQuery(chatId);
25 |
26 | const [addMembers, isLoadingAddMembers] = useAsyncMutation(
27 | useAddGroupMembersMutation
28 | );
29 |
30 | const [selectedMembers, setSelectedMembers] = useState([]);
31 |
32 | const selectMemberHandler = (id) => {
33 | setSelectedMembers((prev) =>
34 | prev.includes(id)
35 | ? prev.filter((currElement) => currElement !== id)
36 | : [...prev, id]
37 | );
38 | };
39 |
40 | const closeHandler = () => {
41 | dispatch(setIsAddMember(false));
42 | };
43 | const addMemberSubmitHandler = () => {
44 | addMembers("Adding Members...", { members: selectedMembers, chatId });
45 | closeHandler();
46 | };
47 |
48 | useErrors([{ isError, error }]);
49 | return (
50 |
89 | );
90 | };
91 |
92 | export default AddMemberDialog;
93 |
--------------------------------------------------------------------------------
/src/pages/admin/ChatManagement.jsx:
--------------------------------------------------------------------------------
1 | import { useFetchData } from "6pp";
2 | import { Avatar, Skeleton, Stack } from "@mui/material";
3 | import React, { useEffect, useState } from "react";
4 | import AdminLayout from "../../components/layout/AdminLayout";
5 | import AvatarCard from "../../components/shared/AvatarCard";
6 | import Table from "../../components/shared/Table";
7 | import { server } from "../../constants/config";
8 | import { useErrors } from "../../hooks/hook";
9 | import { transformImage } from "../../lib/features";
10 |
11 | const columns = [
12 | {
13 | field: "id",
14 | headerName: "ID",
15 | headerClassName: "table-header",
16 | width: 200,
17 | },
18 | {
19 | field: "avatar",
20 | headerName: "Avatar",
21 | headerClassName: "table-header",
22 | width: 150,
23 | renderCell: (params) => ,
24 | },
25 |
26 | {
27 | field: "name",
28 | headerName: "Name",
29 | headerClassName: "table-header",
30 | width: 300,
31 | },
32 |
33 | {
34 | field: "groupChat",
35 | headerName: "Group",
36 | headerClassName: "table-header",
37 | width: 100,
38 | },
39 | {
40 | field: "totalMembers",
41 | headerName: "Total Members",
42 | headerClassName: "table-header",
43 | width: 120,
44 | },
45 | {
46 | field: "members",
47 | headerName: "Members",
48 | headerClassName: "table-header",
49 | width: 400,
50 | renderCell: (params) => (
51 |
52 | ),
53 | },
54 | {
55 | field: "totalMessages",
56 | headerName: "Total Messages",
57 | headerClassName: "table-header",
58 | width: 120,
59 | },
60 | {
61 | field: "creator",
62 | headerName: "Created By",
63 | headerClassName: "table-header",
64 | width: 250,
65 | renderCell: (params) => (
66 |
67 |
68 | {params.row.creator.name}
69 |
70 | ),
71 | },
72 | ];
73 |
74 | const ChatManagement = () => {
75 | const { loading, data, error } = useFetchData(
76 | `${server}/api/v1/admin/chats`,
77 | "dashboard-chats"
78 | );
79 |
80 | useErrors([
81 | {
82 | isError: error,
83 | error: error,
84 | },
85 | ]);
86 |
87 | const [rows, setRows] = useState([]);
88 |
89 | useEffect(() => {
90 | if (data) {
91 | setRows(
92 | data.chats.map((i) => ({
93 | ...i,
94 | id: i._id,
95 | avatar: i.avatar.map((i) => transformImage(i, 50)),
96 | members: i.members.map((i) => transformImage(i.avatar, 50)),
97 | creator: {
98 | name: i.creator.name,
99 | avatar: transformImage(i.creator.avatar, 50),
100 | },
101 | }))
102 | );
103 | }
104 | }, [data]);
105 |
106 | return (
107 |
108 | {loading ? (
109 |
110 | ) : (
111 |
112 | )}
113 |
114 | );
115 | };
116 |
117 | export default ChatManagement;
118 |
--------------------------------------------------------------------------------
/src/components/specific/Notifications.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Avatar,
3 | Button,
4 | Dialog,
5 | DialogTitle,
6 | ListItem,
7 | Skeleton,
8 | Stack,
9 | Typography,
10 | } from "@mui/material";
11 | import React, { memo } from "react";
12 | import { useDispatch, useSelector } from "react-redux";
13 | import { useAsyncMutation, useErrors } from "../../hooks/hook";
14 | import {
15 | useAcceptFriendRequestMutation,
16 | useGetNotificationsQuery,
17 | } from "../../redux/api/api";
18 | import { setIsNotification } from "../../redux/reducers/misc";
19 |
20 | const Notifications = () => {
21 | const { isNotification } = useSelector((state) => state.misc);
22 |
23 | const dispatch = useDispatch();
24 |
25 | const { isLoading, data, error, isError } = useGetNotificationsQuery();
26 |
27 | const [acceptRequest] = useAsyncMutation(useAcceptFriendRequestMutation);
28 |
29 | const friendRequestHandler = async ({ _id, accept }) => {
30 | dispatch(setIsNotification(false));
31 | await acceptRequest("Accepting...", { requestId: _id, accept });
32 | };
33 |
34 | const closeHandler = () => dispatch(setIsNotification(false));
35 |
36 | useErrors([{ error, isError }]);
37 |
38 | return (
39 |
63 | );
64 | };
65 |
66 | const NotificationItem = memo(({ sender, _id, handler }) => {
67 | const { name, avatar } = sender;
68 | return (
69 |
70 |
76 |
77 |
78 |
90 | {`${name} sent you a friend request.`}
91 |
92 |
93 |
99 |
100 |
103 |
104 |
105 |
106 | );
107 | });
108 |
109 | export default Notifications;
110 |
--------------------------------------------------------------------------------
/src/components/specific/NewGroup.jsx:
--------------------------------------------------------------------------------
1 | import { useInputValidation } from "6pp";
2 | import {
3 | Button,
4 | Dialog,
5 | DialogTitle,
6 | Skeleton,
7 | Stack,
8 | TextField,
9 | Typography,
10 | } from "@mui/material";
11 | import React, { useState } from "react";
12 | import { sampleUsers } from "../../constants/sampleData";
13 | import UserItem from "../shared/UserItem";
14 | import { useDispatch, useSelector } from "react-redux";
15 | import {
16 | useAvailableFriendsQuery,
17 | useNewGroupMutation,
18 | } from "../../redux/api/api";
19 | import { useAsyncMutation, useErrors } from "../../hooks/hook";
20 | import { setIsNewGroup } from "../../redux/reducers/misc";
21 | import toast from "react-hot-toast";
22 |
23 | const NewGroup = () => {
24 | const { isNewGroup } = useSelector((state) => state.misc);
25 | const dispatch = useDispatch();
26 |
27 | const { isError, isLoading, error, data } = useAvailableFriendsQuery();
28 | const [newGroup, isLoadingNewGroup] = useAsyncMutation(useNewGroupMutation);
29 |
30 | const groupName = useInputValidation("");
31 |
32 | const [selectedMembers, setSelectedMembers] = useState([]);
33 |
34 | const errors = [
35 | {
36 | isError,
37 | error,
38 | },
39 | ];
40 |
41 | useErrors(errors);
42 |
43 | const selectMemberHandler = (id) => {
44 | setSelectedMembers((prev) =>
45 | prev.includes(id)
46 | ? prev.filter((currElement) => currElement !== id)
47 | : [...prev, id]
48 | );
49 | };
50 |
51 | const submitHandler = () => {
52 | if (!groupName.value) return toast.error("Group name is required");
53 |
54 | if (selectedMembers.length < 2)
55 | return toast.error("Please Select Atleast 3 Members");
56 |
57 | newGroup("Creating New Group...", {
58 | name: groupName.value,
59 | members: selectedMembers,
60 | });
61 |
62 | closeHandler();
63 | };
64 |
65 | const closeHandler = () => {
66 | dispatch(setIsNewGroup(false));
67 | };
68 |
69 | return (
70 |
119 | );
120 | };
121 |
122 | export default NewGroup;
123 |
--------------------------------------------------------------------------------
/src/pages/admin/MessageManagement.jsx:
--------------------------------------------------------------------------------
1 | import { useFetchData } from "6pp";
2 | import { Avatar, Box, Stack } from "@mui/material";
3 | import moment from "moment";
4 | import React, { useEffect, useState } from "react";
5 | import AdminLayout from "../../components/layout/AdminLayout";
6 | import RenderAttachment from "../../components/shared/RenderAttachment";
7 | import Table from "../../components/shared/Table";
8 | import { server } from "../../constants/config";
9 | import { useErrors } from "../../hooks/hook";
10 | import { fileFormat, transformImage } from "../../lib/features";
11 |
12 | const columns = [
13 | {
14 | field: "id",
15 | headerName: "ID",
16 | headerClassName: "table-header",
17 | width: 200,
18 | },
19 | {
20 | field: "attachments",
21 | headerName: "Attachments",
22 | headerClassName: "table-header",
23 | width: 200,
24 | renderCell: (params) => {
25 | const { attachments } = params.row;
26 |
27 | return attachments?.length > 0
28 | ? attachments.map((i) => {
29 | const url = i.url;
30 | const file = fileFormat(url);
31 |
32 | return (
33 |
34 |
42 | {RenderAttachment(file, url)}
43 |
44 |
45 | );
46 | })
47 | : "No Attachments";
48 | },
49 | },
50 |
51 | {
52 | field: "content",
53 | headerName: "Content",
54 | headerClassName: "table-header",
55 | width: 400,
56 | },
57 | {
58 | field: "sender",
59 | headerName: "Sent By",
60 | headerClassName: "table-header",
61 | width: 200,
62 | renderCell: (params) => (
63 |
64 |
65 | {params.row.sender.name}
66 |
67 | ),
68 | },
69 | {
70 | field: "chat",
71 | headerName: "Chat",
72 | headerClassName: "table-header",
73 | width: 220,
74 | },
75 | {
76 | field: "groupChat",
77 | headerName: "Group Chat",
78 | headerClassName: "table-header",
79 | width: 100,
80 | },
81 | {
82 | field: "createdAt",
83 | headerName: "Time",
84 | headerClassName: "table-header",
85 | width: 250,
86 | },
87 | ];
88 |
89 | const MessageManagement = () => {
90 | const { loading, data, error } = useFetchData(
91 | `${server}/api/v1/admin/messages`,
92 | "dashboard-messages"
93 | );
94 |
95 | useErrors([
96 | {
97 | isError: error,
98 | error: error,
99 | },
100 | ]);
101 |
102 | const [rows, setRows] = useState([]);
103 |
104 | useEffect(() => {
105 | if (data) {
106 | setRows(
107 | data.messages.map((i) => ({
108 | ...i,
109 | id: i._id,
110 | sender: {
111 | name: i.sender.name,
112 | avatar: transformImage(i.sender.avatar, 50),
113 | },
114 | createdAt: moment(i.createdAt).format("MMMM Do YYYY, h:mm:ss a"),
115 | }))
116 | );
117 | }
118 | }, [data]);
119 |
120 | return (
121 |
122 | {loading ? (
123 |
124 | ) : (
125 |
131 | )}
132 |
133 | );
134 | };
135 |
136 | export default MessageManagement;
137 |
--------------------------------------------------------------------------------
/src/components/layout/AdminLayout.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Close as CloseIcon,
3 | Dashboard as DashboardIcon,
4 | ExitToApp as ExitToAppIcon,
5 | Groups as GroupsIcon,
6 | ManageAccounts as ManageAccountsIcon,
7 | Menu as MenuIcon,
8 | Message as MessageIcon,
9 | } from "@mui/icons-material";
10 | import {
11 | Box,
12 | Drawer,
13 | Grid,
14 | IconButton,
15 | Stack,
16 | Typography,
17 | styled,
18 | } from "@mui/material";
19 | import React, { useState } from "react";
20 | import { Link as LinkComponent, Navigate, useLocation } from "react-router-dom";
21 | import { grayColor, matBlack } from "../../constants/color";
22 | import { useDispatch, useSelector } from "react-redux";
23 | import { adminLogout } from "../../redux/thunks/admin";
24 |
25 | const Link = styled(LinkComponent)`
26 | text-decoration: none;
27 | border-radius: 2rem;
28 | padding: 1rem 2rem;
29 | color: black;
30 | &:hover {
31 | color: rgba(0, 0, 0, 0.54);
32 | }
33 | `;
34 |
35 | const adminTabs = [
36 | {
37 | name: "Dashboard",
38 | path: "/admin/dashboard",
39 | icon: ,
40 | },
41 | {
42 | name: "Users",
43 | path: "/admin/users",
44 | icon: ,
45 | },
46 | {
47 | name: "Chats",
48 | path: "/admin/chats",
49 | icon: ,
50 | },
51 | {
52 | name: "Messages",
53 | path: "/admin/messages",
54 | icon: ,
55 | },
56 | ];
57 |
58 | const Sidebar = ({ w = "100%" }) => {
59 | const location = useLocation();
60 | const dispatch = useDispatch();
61 |
62 | const logoutHandler = () => {
63 | dispatch(adminLogout());
64 | };
65 |
66 | return (
67 |
68 |
69 | Chattu
70 |
71 |
72 |
73 | {adminTabs.map((tab) => (
74 |
85 |
86 | {tab.icon}
87 |
88 | {tab.name}
89 |
90 |
91 | ))}
92 |
93 |
94 |
95 |
96 |
97 | Logout
98 |
99 |
100 |
101 |
102 | );
103 | };
104 |
105 | const AdminLayout = ({ children }) => {
106 | const { isAdmin } = useSelector((state) => state.auth);
107 |
108 | const [isMobile, setIsMobile] = useState(false);
109 |
110 | const handleMobile = () => setIsMobile(!isMobile);
111 |
112 | const handleClose = () => setIsMobile(false);
113 |
114 | if (!isAdmin) return ;
115 |
116 | return (
117 |
118 |
126 |
127 | {isMobile ? : }
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
144 | {children}
145 |
146 |
147 |
148 |
149 |
150 |
151 | );
152 | };
153 |
154 | export default AdminLayout;
155 |
--------------------------------------------------------------------------------
/src/constants/sampleData.js:
--------------------------------------------------------------------------------
1 | export const samepleChats = [
2 | {
3 | avatar: ["https://www.w3schools.com/howto/img_avatar.png"],
4 | name: "John Doe",
5 | _id: "1",
6 | groupChat: false,
7 | members: ["1", "2"],
8 | },
9 |
10 | {
11 | avatar: ["https://www.w3schools.com/howto/img_avatar.png"],
12 | name: "John Boi",
13 | _id: "2",
14 | groupChat: true,
15 | members: ["1", "2"],
16 | },
17 | ];
18 |
19 | export const sampleUsers = [
20 | {
21 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
22 | name: "John Doe",
23 | _id: "1",
24 | },
25 | {
26 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
27 | name: "John Boi",
28 | _id: "2",
29 | },
30 | ];
31 |
32 | export const sampleNotifications = [
33 | {
34 | sender: {
35 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
36 | name: "John Doe",
37 | },
38 | _id: "1",
39 | },
40 | {
41 | sender: {
42 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
43 | name: "John Boi",
44 | },
45 | _id: "2",
46 | },
47 | ];
48 |
49 | export const sampleMessage = [
50 | {
51 | attachments: [],
52 | content: "L*uda ka Message hai",
53 | _id: "sfnsdjkfsdnfkjsbnd",
54 | sender: {
55 | _id: "user._id",
56 | name: "Chaman ",
57 | },
58 | chat: "chatId",
59 | createdAt: "2024-02-12T10:41:30.630Z",
60 | },
61 |
62 | {
63 | attachments: [
64 | {
65 | public_id: "asdsad 2",
66 | url: "https://www.w3schools.com/howto/img_avatar.png",
67 | },
68 | ],
69 | content: "",
70 | _id: "sfnsdjkfsdnfkdddjsbnd",
71 | sender: {
72 | _id: "sdfsdfsdf",
73 | name: "Chaman 2",
74 | },
75 | chat: "chatId",
76 | createdAt: "2024-02-12T10:41:30.630Z",
77 | },
78 | ];
79 |
80 | export const dashboardData = {
81 | users: [
82 | {
83 | name: "John Doe",
84 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
85 | _id: "1",
86 | username: "john_doe",
87 | friends: 20,
88 | groups: 5,
89 | },
90 | {
91 | name: "John Boi",
92 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
93 | _id: "2",
94 | username: "john_boi",
95 | friends: 20,
96 | groups: 25,
97 | },
98 | ],
99 |
100 | chats: [
101 | {
102 | name: "LabadBass Group",
103 | avatar: ["https://www.w3schools.com/howto/img_avatar.png"],
104 | _id: "1",
105 | groupChat: false,
106 | members: [
107 | { _id: "1", avatar: "https://www.w3schools.com/howto/img_avatar.png" },
108 | { _id: "2", avatar: "https://www.w3schools.com/howto/img_avatar.png" },
109 | ],
110 | totalMembers: 2,
111 | totalMessages: 20,
112 | creator: {
113 | name: "John Doe",
114 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
115 | },
116 | },
117 | {
118 | name: "L*Da Luston Group",
119 | avatar: ["https://www.w3schools.com/howto/img_avatar.png"],
120 | _id: "2",
121 | groupChat: true,
122 | members: [
123 | { _id: "1", avatar: "https://www.w3schools.com/howto/img_avatar.png" },
124 | { _id: "2", avatar: "https://www.w3schools.com/howto/img_avatar.png" },
125 | ],
126 | totalMembers: 2,
127 | totalMessages: 20,
128 | creator: {
129 | name: "John Boi",
130 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
131 | },
132 | },
133 | ],
134 |
135 | messages: [
136 | {
137 | attachments: [],
138 | content: "L*uda ka Message hai",
139 | _id: "sfnsdjkfsdnfkjsbnd",
140 | sender: {
141 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
142 | name: "Chaman ",
143 | },
144 | chat: "chatId",
145 | groupChat: false,
146 | createdAt: "2024-02-12T10:41:30.630Z",
147 | },
148 |
149 | {
150 | attachments: [
151 | {
152 | public_id: "asdsad 2",
153 | url: "https://www.w3schools.com/howto/img_avatar.png",
154 | },
155 | ],
156 | content: "",
157 | _id: "sfnsdjkfsdnfkdddjsbnd",
158 | sender: {
159 | avatar: "https://www.w3schools.com/howto/img_avatar.png",
160 | name: "Chaman 2",
161 | },
162 | chat: "chatId",
163 | groupChat: true,
164 | createdAt: "2024-02-12T10:41:30.630Z",
165 | },
166 | ],
167 | };
168 |
--------------------------------------------------------------------------------
/src/components/dialogs/FileMenu.jsx:
--------------------------------------------------------------------------------
1 | import { ListItemText, Menu, MenuItem, MenuList, Tooltip } from "@mui/material";
2 | import React, { useRef } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { setIsFileMenu, setUploadingLoader } from "../../redux/reducers/misc";
5 | import {
6 | AudioFile as AudioFileIcon,
7 | Image as ImageIcon,
8 | UploadFile as UploadFileIcon,
9 | VideoFile as VideoFileIcon,
10 | } from "@mui/icons-material";
11 | import toast from "react-hot-toast";
12 | import { useSendAttachmentsMutation } from "../../redux/api/api";
13 |
14 | const FileMenu = ({ anchorE1, chatId }) => {
15 | const { isFileMenu } = useSelector((state) => state.misc);
16 |
17 | const dispatch = useDispatch();
18 |
19 | const imageRef = useRef(null);
20 | const audioRef = useRef(null);
21 | const videoRef = useRef(null);
22 | const fileRef = useRef(null);
23 |
24 | const [sendAttachments] = useSendAttachmentsMutation();
25 |
26 | const closeFileMenu = () => dispatch(setIsFileMenu(false));
27 |
28 | const selectImage = () => imageRef.current?.click();
29 | const selectAudio = () => audioRef.current?.click();
30 | const selectVideo = () => videoRef.current?.click();
31 | const selectFile = () => fileRef.current?.click();
32 |
33 | const fileChangeHandler = async (e, key) => {
34 | const files = Array.from(e.target.files);
35 |
36 | if (files.length <= 0) return;
37 |
38 | if (files.length > 5)
39 | return toast.error(`You can only send 5 ${key} at a time`);
40 |
41 | dispatch(setUploadingLoader(true));
42 |
43 | const toastId = toast.loading(`Sending ${key}...`);
44 | closeFileMenu();
45 |
46 | try {
47 | const myForm = new FormData();
48 |
49 | myForm.append("chatId", chatId);
50 | files.forEach((file) => myForm.append("files", file));
51 |
52 | const res = await sendAttachments(myForm);
53 |
54 | if (res.data) toast.success(`${key} sent successfully`, { id: toastId });
55 | else toast.error(`Failed to send ${key}`, { id: toastId });
56 |
57 | // Fetching Here
58 | } catch (error) {
59 | toast.error(error, { id: toastId });
60 | } finally {
61 | dispatch(setUploadingLoader(false));
62 | }
63 | };
64 |
65 | return (
66 |
135 | );
136 | };
137 |
138 | export default FileMenu;
139 |
--------------------------------------------------------------------------------
/src/redux/api/api.js:
--------------------------------------------------------------------------------
1 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
2 | import { server } from "../../constants/config";
3 |
4 | const api = createApi({
5 | reducerPath: "api",
6 | baseQuery: fetchBaseQuery({ baseUrl: `${server}/api/v1/` }),
7 | tagTypes: ["Chat", "User", "Message"],
8 |
9 | endpoints: (builder) => ({
10 | myChats: builder.query({
11 | query: () => ({
12 | url: "chat/my",
13 | credentials: "include",
14 | }),
15 | providesTags: ["Chat"],
16 | }),
17 |
18 | searchUser: builder.query({
19 | query: (name) => ({
20 | url: `user/search?name=${name}`,
21 | credentials: "include",
22 | }),
23 | providesTags: ["User"],
24 | }),
25 |
26 | sendFriendRequest: builder.mutation({
27 | query: (data) => ({
28 | url: "user/sendrequest",
29 | method: "PUT",
30 | credentials: "include",
31 | body: data,
32 | }),
33 | invalidatesTags: ["User"],
34 | }),
35 |
36 | getNotifications: builder.query({
37 | query: () => ({
38 | url: `user/notifications`,
39 | credentials: "include",
40 | }),
41 | keepUnusedDataFor: 0,
42 | }),
43 |
44 | acceptFriendRequest: builder.mutation({
45 | query: (data) => ({
46 | url: "user/acceptrequest",
47 | method: "PUT",
48 | credentials: "include",
49 | body: data,
50 | }),
51 | invalidatesTags: ["Chat"],
52 | }),
53 |
54 | chatDetails: builder.query({
55 | query: ({ chatId, populate = false }) => {
56 | let url = `chat/${chatId}`;
57 | if (populate) url += "?populate=true";
58 |
59 | return {
60 | url,
61 | credentials: "include",
62 | };
63 | },
64 | providesTags: ["Chat"],
65 | }),
66 |
67 | getMessages: builder.query({
68 | query: ({ chatId, page }) => ({
69 | url: `chat/message/${chatId}?page=${page}`,
70 | credentials: "include",
71 | }),
72 | keepUnusedDataFor: 0,
73 | }),
74 |
75 | sendAttachments: builder.mutation({
76 | query: (data) => ({
77 | url: "chat/message",
78 | method: "POST",
79 | credentials: "include",
80 | body: data,
81 | }),
82 | }),
83 |
84 | myGroups: builder.query({
85 | query: () => ({
86 | url: "chat/my/groups",
87 | credentials: "include",
88 | }),
89 | providesTags: ["Chat"],
90 | }),
91 |
92 | availableFriends: builder.query({
93 | query: (chatId) => {
94 | let url = `user/friends`;
95 | if (chatId) url += `?chatId=${chatId}`;
96 |
97 | return {
98 | url,
99 | credentials: "include",
100 | };
101 | },
102 | providesTags: ["Chat"],
103 | }),
104 |
105 | newGroup: builder.mutation({
106 | query: ({ name, members }) => ({
107 | url: "chat/new",
108 | method: "POST",
109 | credentials: "include",
110 | body: { name, members },
111 | }),
112 | invalidatesTags: ["Chat"],
113 | }),
114 |
115 | renameGroup: builder.mutation({
116 | query: ({ chatId, name }) => ({
117 | url: `chat/${chatId}`,
118 | method: "PUT",
119 | credentials: "include",
120 | body: { name },
121 | }),
122 | invalidatesTags: ["Chat"],
123 | }),
124 |
125 | removeGroupMember: builder.mutation({
126 | query: ({ chatId, userId }) => ({
127 | url: `chat/removemember`,
128 | method: "PUT",
129 | credentials: "include",
130 | body: { chatId, userId },
131 | }),
132 | invalidatesTags: ["Chat"],
133 | }),
134 |
135 | addGroupMembers: builder.mutation({
136 | query: ({ members, chatId }) => ({
137 | url: `chat/addmembers`,
138 | method: "PUT",
139 | credentials: "include",
140 | body: { members, chatId },
141 | }),
142 | invalidatesTags: ["Chat"],
143 | }),
144 |
145 | deleteChat: builder.mutation({
146 | query: (chatId) => ({
147 | url: `chat/${chatId}`,
148 | method: "DELETE",
149 | credentials: "include",
150 | }),
151 | invalidatesTags: ["Chat"],
152 | }),
153 |
154 | leaveGroup: builder.mutation({
155 | query: (chatId) => ({
156 | url: `chat/leave/${chatId}`,
157 | method: "DELETE",
158 | credentials: "include",
159 | }),
160 | invalidatesTags: ["Chat"],
161 | }),
162 | }),
163 | });
164 |
165 | export default api;
166 | export const {
167 | useMyChatsQuery,
168 | useLazySearchUserQuery,
169 | useSendFriendRequestMutation,
170 | useGetNotificationsQuery,
171 | useAcceptFriendRequestMutation,
172 | useChatDetailsQuery,
173 | useGetMessagesQuery,
174 | useSendAttachmentsMutation,
175 | useMyGroupsQuery,
176 | useAvailableFriendsQuery,
177 | useNewGroupMutation,
178 | useRenameGroupMutation,
179 | useRemoveGroupMemberMutation,
180 | useAddGroupMembersMutation,
181 | useDeleteChatMutation,
182 | useLeaveGroupMutation,
183 | } = api;
184 |
--------------------------------------------------------------------------------
/src/components/layout/AppLayout.jsx:
--------------------------------------------------------------------------------
1 | import { Drawer, Grid, Skeleton } from "@mui/material";
2 | import React, { useCallback, useEffect, useRef, useState } from "react";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { useNavigate, useParams } from "react-router-dom";
5 | import {
6 | NEW_MESSAGE_ALERT,
7 | NEW_REQUEST,
8 | ONLINE_USERS,
9 | REFETCH_CHATS,
10 | } from "../../constants/events";
11 | import { useErrors, useSocketEvents } from "../../hooks/hook";
12 | import { getOrSaveFromStorage } from "../../lib/features";
13 | import { useMyChatsQuery } from "../../redux/api/api";
14 | import {
15 | incrementNotification,
16 | setNewMessagesAlert,
17 | } from "../../redux/reducers/chat";
18 | import {
19 | setIsDeleteMenu,
20 | setIsMobile,
21 | setSelectedDeleteChat,
22 | } from "../../redux/reducers/misc";
23 | import { getSocket } from "../../socket";
24 | import DeleteChatMenu from "../dialogs/DeleteChatMenu";
25 | import Title from "../shared/Title";
26 | import ChatList from "../specific/ChatList";
27 | import Profile from "../specific/Profile";
28 | import Header from "./Header";
29 |
30 | const AppLayout = () => (WrappedComponent) => {
31 | return (props) => {
32 | const params = useParams();
33 | const navigate = useNavigate();
34 | const dispatch = useDispatch();
35 | const socket = getSocket();
36 |
37 | const chatId = params.chatId;
38 | const deleteMenuAnchor = useRef(null);
39 |
40 | const [onlineUsers, setOnlineUsers] = useState([]);
41 |
42 | const { isMobile } = useSelector((state) => state.misc);
43 | const { user } = useSelector((state) => state.auth);
44 | const { newMessagesAlert } = useSelector((state) => state.chat);
45 |
46 | const { isLoading, data, isError, error, refetch } = useMyChatsQuery("");
47 |
48 | useErrors([{ isError, error }]);
49 |
50 | useEffect(() => {
51 | getOrSaveFromStorage({ key: NEW_MESSAGE_ALERT, value: newMessagesAlert });
52 | }, [newMessagesAlert]);
53 |
54 | const handleDeleteChat = (e, chatId, groupChat) => {
55 | dispatch(setIsDeleteMenu(true));
56 | dispatch(setSelectedDeleteChat({ chatId, groupChat }));
57 | deleteMenuAnchor.current = e.currentTarget;
58 | };
59 |
60 | const handleMobileClose = () => dispatch(setIsMobile(false));
61 |
62 | const newMessageAlertListener = useCallback(
63 | (data) => {
64 | if (data.chatId === chatId) return;
65 | dispatch(setNewMessagesAlert(data));
66 | },
67 | [chatId]
68 | );
69 |
70 | const newRequestListener = useCallback(() => {
71 | dispatch(incrementNotification());
72 | }, [dispatch]);
73 |
74 | const refetchListener = useCallback(() => {
75 | refetch();
76 | navigate("/");
77 | }, [refetch, navigate]);
78 |
79 | const onlineUsersListener = useCallback((data) => {
80 | setOnlineUsers(data);
81 | }, []);
82 |
83 | const eventHandlers = {
84 | [NEW_MESSAGE_ALERT]: newMessageAlertListener,
85 | [NEW_REQUEST]: newRequestListener,
86 | [REFETCH_CHATS]: refetchListener,
87 | [ONLINE_USERS]: onlineUsersListener,
88 | };
89 |
90 | useSocketEvents(socket, eventHandlers);
91 |
92 | return (
93 | <>
94 |
95 |
96 |
97 |
101 |
102 | {isLoading ? (
103 |
104 | ) : (
105 |
106 |
114 |
115 | )}
116 |
117 |
118 |
127 | {isLoading ? (
128 |
129 | ) : (
130 |
137 | )}
138 |
139 |
140 |
141 |
142 |
143 |
154 |
155 |
156 |
157 | >
158 | );
159 | };
160 | };
161 |
162 | export default AppLayout;
163 |
--------------------------------------------------------------------------------
/src/components/layout/Header.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | AppBar,
3 | Backdrop,
4 | Badge,
5 | Box,
6 | IconButton,
7 | Toolbar,
8 | Tooltip,
9 | Typography,
10 | } from "@mui/material";
11 | import React, { Suspense, lazy, useState } from "react";
12 | import { orange } from "../../constants/color";
13 | import {
14 | Add as AddIcon,
15 | Menu as MenuIcon,
16 | Search as SearchIcon,
17 | Group as GroupIcon,
18 | Logout as LogoutIcon,
19 | Notifications as NotificationsIcon,
20 | } from "@mui/icons-material";
21 | import { useNavigate } from "react-router-dom";
22 | import axios from "axios";
23 | import { server } from "../../constants/config";
24 | import toast from "react-hot-toast";
25 | import { useDispatch, useSelector } from "react-redux";
26 | import { userNotExists } from "../../redux/reducers/auth";
27 | import {
28 | setIsMobile,
29 | setIsNewGroup,
30 | setIsNotification,
31 | setIsSearch,
32 | } from "../../redux/reducers/misc";
33 | import { resetNotificationCount } from "../../redux/reducers/chat";
34 |
35 | const SearchDialog = lazy(() => import("../specific/Search"));
36 | const NotifcationDialog = lazy(() => import("../specific/Notifications"));
37 | const NewGroupDialog = lazy(() => import("../specific/NewGroup"));
38 |
39 | const Header = () => {
40 | const navigate = useNavigate();
41 | const dispatch = useDispatch();
42 |
43 | const { isSearch, isNotification, isNewGroup } = useSelector(
44 | (state) => state.misc
45 | );
46 | const { notificationCount } = useSelector((state) => state.chat);
47 |
48 | const handleMobile = () => dispatch(setIsMobile(true));
49 |
50 | const openSearch = () => dispatch(setIsSearch(true));
51 |
52 | const openNewGroup = () => {
53 | dispatch(setIsNewGroup(true));
54 | };
55 |
56 | const openNotification = () => {
57 | dispatch(setIsNotification(true));
58 | dispatch(resetNotificationCount());
59 | };
60 |
61 | const navigateToGroup = () => navigate("/groups");
62 |
63 | const logoutHandler = async () => {
64 | try {
65 | const { data } = await axios.get(`${server}/api/v1/user/logout`, {
66 | withCredentials: true,
67 | });
68 | dispatch(userNotExists());
69 | toast.success(data.message);
70 | } catch (error) {
71 | toast.error(error?.response?.data?.message || "Something went wrong");
72 | }
73 | };
74 |
75 | return (
76 | <>
77 |
78 |
84 |
85 |
91 | Chattu
92 |
93 |
94 |
99 |
100 |
101 |
102 |
103 |
108 |
109 | }
112 | onClick={openSearch}
113 | />
114 |
115 | }
118 | onClick={openNewGroup}
119 | />
120 |
121 | }
124 | onClick={navigateToGroup}
125 | />
126 |
127 | }
130 | onClick={openNotification}
131 | value={notificationCount}
132 | />
133 |
134 | }
137 | onClick={logoutHandler}
138 | />
139 |
140 |
141 |
142 |
143 |
144 | {isSearch && (
145 | }>
146 |
147 |
148 | )}
149 |
150 | {isNotification && (
151 | }>
152 |
153 |
154 | )}
155 |
156 | {isNewGroup && (
157 | }>
158 |
159 |
160 | )}
161 | >
162 | );
163 | };
164 |
165 | const IconBtn = ({ title, icon, onClick, value }) => {
166 | return (
167 |
168 |
169 | {value ? (
170 |
171 | {icon}
172 |
173 | ) : (
174 | icon
175 | )}
176 |
177 |
178 | );
179 | };
180 |
181 | export default Header;
182 |
--------------------------------------------------------------------------------
/src/pages/admin/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { useFetchData } from "6pp";
2 | import {
3 | AdminPanelSettings as AdminPanelSettingsIcon,
4 | Group as GroupIcon,
5 | Message as MessageIcon,
6 | Notifications as NotificationsIcon,
7 | Person as PersonIcon,
8 | } from "@mui/icons-material";
9 | import {
10 | Box,
11 | Container,
12 | Paper,
13 | Skeleton,
14 | Stack,
15 | Typography,
16 | } from "@mui/material";
17 | import moment from "moment";
18 | import React from "react";
19 | import AdminLayout from "../../components/layout/AdminLayout";
20 | import { DoughnutChart, LineChart } from "../../components/specific/Charts";
21 | import {
22 | CurveButton,
23 | SearchField,
24 | } from "../../components/styles/StyledComponents";
25 | import { matBlack } from "../../constants/color";
26 | import { server } from "../../constants/config";
27 | import { useErrors } from "../../hooks/hook";
28 |
29 | const Dashboard = () => {
30 | const { loading, data, error } = useFetchData(
31 | `${server}/api/v1/admin/stats`,
32 | "dashboard-stats"
33 | );
34 |
35 | const { stats } = data || {};
36 |
37 | useErrors([
38 | {
39 | isError: error,
40 | error: error,
41 | },
42 | ]);
43 |
44 | const Appbar = (
45 |
49 |
50 |
51 |
52 |
53 |
54 | Search
55 |
56 |
64 | {moment().format("dddd, D MMMM YYYY")}
65 |
66 |
67 |
68 |
69 |
70 | );
71 |
72 | const Widgets = (
73 |
83 | } />
84 | }
88 | />
89 | }
93 | />
94 |
95 | );
96 |
97 | return (
98 |
99 | {loading ? (
100 |
101 | ) : (
102 |
103 | {Appbar}
104 |
105 |
118 |
127 |
128 | Last Messages
129 |
130 |
131 |
132 |
133 |
134 |
147 |
154 |
155 |
164 | Vs
165 |
166 |
167 |
168 |
169 |
170 | {Widgets}
171 |
172 | )}
173 |
174 | );
175 | };
176 |
177 | const Widget = ({ title, value, Icon }) => (
178 |
187 |
188 |
200 | {value}
201 |
202 |
203 | {Icon}
204 | {title}
205 |
206 |
207 |
208 | );
209 |
210 | export default Dashboard;
211 |
--------------------------------------------------------------------------------
/src/pages/Chat.jsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | Fragment,
3 | useCallback,
4 | useEffect,
5 | useRef,
6 | useState,
7 | } from "react";
8 | import AppLayout from "../components/layout/AppLayout";
9 | import { IconButton, Skeleton, Stack } from "@mui/material";
10 | import { grayColor, orange } from "../constants/color";
11 | import {
12 | AttachFile as AttachFileIcon,
13 | Send as SendIcon,
14 | } from "@mui/icons-material";
15 | import { InputBox } from "../components/styles/StyledComponents";
16 | import FileMenu from "../components/dialogs/FileMenu";
17 | import MessageComponent from "../components/shared/MessageComponent";
18 | import { getSocket } from "../socket";
19 | import {
20 | ALERT,
21 | CHAT_JOINED,
22 | CHAT_LEAVED,
23 | NEW_MESSAGE,
24 | START_TYPING,
25 | STOP_TYPING,
26 | } from "../constants/events";
27 | import { useChatDetailsQuery, useGetMessagesQuery } from "../redux/api/api";
28 | import { useErrors, useSocketEvents } from "../hooks/hook";
29 | import { useInfiniteScrollTop } from "6pp";
30 | import { useDispatch } from "react-redux";
31 | import { setIsFileMenu } from "../redux/reducers/misc";
32 | import { removeNewMessagesAlert } from "../redux/reducers/chat";
33 | import { TypingLoader } from "../components/layout/Loaders";
34 | import { useNavigate } from "react-router-dom";
35 |
36 | const Chat = ({ chatId, user }) => {
37 | const socket = getSocket();
38 | const dispatch = useDispatch();
39 | const navigate = useNavigate();
40 |
41 | const containerRef = useRef(null);
42 | const bottomRef = useRef(null);
43 |
44 | const [message, setMessage] = useState("");
45 | const [messages, setMessages] = useState([]);
46 | const [page, setPage] = useState(1);
47 | const [fileMenuAnchor, setFileMenuAnchor] = useState(null);
48 |
49 | const [IamTyping, setIamTyping] = useState(false);
50 | const [userTyping, setUserTyping] = useState(false);
51 | const typingTimeout = useRef(null);
52 |
53 | const chatDetails = useChatDetailsQuery({ chatId, skip: !chatId });
54 |
55 | const oldMessagesChunk = useGetMessagesQuery({ chatId, page });
56 |
57 | const { data: oldMessages, setData: setOldMessages } = useInfiniteScrollTop(
58 | containerRef,
59 | oldMessagesChunk.data?.totalPages,
60 | page,
61 | setPage,
62 | oldMessagesChunk.data?.messages
63 | );
64 |
65 | const errors = [
66 | { isError: chatDetails.isError, error: chatDetails.error },
67 | { isError: oldMessagesChunk.isError, error: oldMessagesChunk.error },
68 | ];
69 |
70 | const members = chatDetails?.data?.chat?.members;
71 |
72 | const messageOnChange = (e) => {
73 | setMessage(e.target.value);
74 |
75 | if (!IamTyping) {
76 | socket.emit(START_TYPING, { members, chatId });
77 | setIamTyping(true);
78 | }
79 |
80 | if (typingTimeout.current) clearTimeout(typingTimeout.current);
81 |
82 | typingTimeout.current = setTimeout(() => {
83 | socket.emit(STOP_TYPING, { members, chatId });
84 | setIamTyping(false);
85 | }, [2000]);
86 | };
87 |
88 | const handleFileOpen = (e) => {
89 | dispatch(setIsFileMenu(true));
90 | setFileMenuAnchor(e.currentTarget);
91 | };
92 |
93 | const submitHandler = (e) => {
94 | e.preventDefault();
95 |
96 | if (!message.trim()) return;
97 |
98 | // Emitting the message to the server
99 | socket.emit(NEW_MESSAGE, { chatId, members, message });
100 | setMessage("");
101 | };
102 |
103 | useEffect(() => {
104 | socket.emit(CHAT_JOINED, { userId: user._id, members });
105 | dispatch(removeNewMessagesAlert(chatId));
106 |
107 | return () => {
108 | setMessages([]);
109 | setMessage("");
110 | setOldMessages([]);
111 | setPage(1);
112 | socket.emit(CHAT_LEAVED, { userId: user._id, members });
113 | };
114 | }, [chatId]);
115 |
116 | useEffect(() => {
117 | if (bottomRef.current)
118 | bottomRef.current.scrollIntoView({ behavior: "smooth" });
119 | }, [messages]);
120 |
121 | useEffect(() => {
122 | if (chatDetails.isError) return navigate("/");
123 | }, [chatDetails.isError]);
124 |
125 | const newMessagesListener = useCallback(
126 | (data) => {
127 | if (data.chatId !== chatId) return;
128 |
129 | setMessages((prev) => [...prev, data.message]);
130 | },
131 | [chatId]
132 | );
133 |
134 | const startTypingListener = useCallback(
135 | (data) => {
136 | if (data.chatId !== chatId) return;
137 |
138 | setUserTyping(true);
139 | },
140 | [chatId]
141 | );
142 |
143 | const stopTypingListener = useCallback(
144 | (data) => {
145 | if (data.chatId !== chatId) return;
146 | setUserTyping(false);
147 | },
148 | [chatId]
149 | );
150 |
151 | const alertListener = useCallback(
152 | (data) => {
153 | if (data.chatId !== chatId) return;
154 | const messageForAlert = {
155 | content: data.message,
156 | sender: {
157 | _id: "djasdhajksdhasdsadasdas",
158 | name: "Admin",
159 | },
160 | chat: chatId,
161 | createdAt: new Date().toISOString(),
162 | };
163 |
164 | setMessages((prev) => [...prev, messageForAlert]);
165 | },
166 | [chatId]
167 | );
168 |
169 | const eventHandler = {
170 | [ALERT]: alertListener,
171 | [NEW_MESSAGE]: newMessagesListener,
172 | [START_TYPING]: startTypingListener,
173 | [STOP_TYPING]: stopTypingListener,
174 | };
175 |
176 | useSocketEvents(socket, eventHandler);
177 |
178 | useErrors(errors);
179 |
180 | const allMessages = [...oldMessages, ...messages];
181 |
182 | return chatDetails.isLoading ? (
183 |
184 | ) : (
185 |
186 |
198 | {allMessages.map((i) => (
199 |
200 | ))}
201 |
202 | {userTyping && }
203 |
204 |
205 |
206 |
207 |
254 |
255 |
256 |
257 | );
258 | };
259 |
260 | export default AppLayout()(Chat);
261 |
--------------------------------------------------------------------------------
/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useFileHandler, useInputValidation } from "6pp";
2 | import { CameraAlt as CameraAltIcon } from "@mui/icons-material";
3 | import {
4 | Avatar,
5 | Button,
6 | Container,
7 | IconButton,
8 | Paper,
9 | Stack,
10 | TextField,
11 | Typography,
12 | } from "@mui/material";
13 | import axios from "axios";
14 | import React, { useState } from "react";
15 | import toast from "react-hot-toast";
16 | import { useDispatch } from "react-redux";
17 | import { VisuallyHiddenInput } from "../components/styles/StyledComponents";
18 | import { bgGradient } from "../constants/color";
19 | import { server } from "../constants/config";
20 | import { userExists } from "../redux/reducers/auth";
21 | import { usernameValidator } from "../utils/validators";
22 |
23 | const Login = () => {
24 | const [isLogin, setIsLogin] = useState(true);
25 | const [isLoading, setIsLoading] = useState(false);
26 |
27 | const toggleLogin = () => setIsLogin((prev) => !prev);
28 |
29 | const name = useInputValidation("");
30 | const bio = useInputValidation("");
31 | const username = useInputValidation("", usernameValidator);
32 | const password = useInputValidation("");
33 |
34 | const avatar = useFileHandler("single");
35 |
36 | const dispatch = useDispatch();
37 |
38 | const handleLogin = async (e) => {
39 | e.preventDefault();
40 |
41 | const toastId = toast.loading("Logging In...");
42 |
43 | setIsLoading(true);
44 | const config = {
45 | withCredentials: true,
46 | headers: {
47 | "Content-Type": "application/json",
48 | },
49 | };
50 |
51 | try {
52 | const { data } = await axios.post(
53 | `${server}/api/v1/user/login`,
54 | {
55 | username: username.value,
56 | password: password.value,
57 | },
58 | config
59 | );
60 | dispatch(userExists(data.user));
61 | toast.success(data.message, {
62 | id: toastId,
63 | });
64 | } catch (error) {
65 | toast.error(error?.response?.data?.message || "Something Went Wrong", {
66 | id: toastId,
67 | });
68 | } finally {
69 | setIsLoading(false);
70 | }
71 | };
72 |
73 | const handleSignUp = async (e) => {
74 | e.preventDefault();
75 |
76 | const toastId = toast.loading("Signing Up...");
77 | setIsLoading(true);
78 |
79 | const formData = new FormData();
80 | formData.append("avatar", avatar.file);
81 | formData.append("name", name.value);
82 | formData.append("bio", bio.value);
83 | formData.append("username", username.value);
84 | formData.append("password", password.value);
85 |
86 | const config = {
87 | withCredentials: true,
88 | headers: {
89 | "Content-Type": "multipart/form-data",
90 | },
91 | };
92 |
93 | try {
94 | const { data } = await axios.post(
95 | `${server}/api/v1/user/new`,
96 | formData,
97 | config
98 | );
99 |
100 | dispatch(userExists(data.user));
101 | toast.success(data.message, {
102 | id: toastId,
103 | });
104 | } catch (error) {
105 | toast.error(error?.response?.data?.message || "Something Went Wrong", {
106 | id: toastId,
107 | });
108 | } finally {
109 | setIsLoading(false);
110 | }
111 | };
112 |
113 | return (
114 |
119 |
129 |
138 | {isLogin ? (
139 | <>
140 | Login
141 |
195 | >
196 | ) : (
197 | <>
198 | Sign Up
199 |
323 | >
324 | )}
325 |
326 |
327 |
328 | );
329 | };
330 |
331 | export default Login;
332 |
--------------------------------------------------------------------------------
/src/pages/Groups.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Add as AddIcon,
3 | Delete as DeleteIcon,
4 | Done as DoneIcon,
5 | Edit as EditIcon,
6 | KeyboardBackspace as KeyboardBackspaceIcon,
7 | Menu as MenuIcon,
8 | } from "@mui/icons-material";
9 | import {
10 | Backdrop,
11 | Box,
12 | Button,
13 | CircularProgress,
14 | Drawer,
15 | Grid,
16 | IconButton,
17 | Stack,
18 | TextField,
19 | Tooltip,
20 | Typography,
21 | } from "@mui/material";
22 | import React, { Suspense, lazy, memo, useEffect, useState } from "react";
23 | import { useNavigate, useSearchParams } from "react-router-dom";
24 | import { LayoutLoader } from "../components/layout/Loaders";
25 | import AvatarCard from "../components/shared/AvatarCard";
26 | import { Link } from "../components/styles/StyledComponents";
27 | import { bgGradient, matBlack } from "../constants/color";
28 | import { useDispatch, useSelector } from "react-redux";
29 | import UserItem from "../components/shared/UserItem";
30 | import { useAsyncMutation, useErrors } from "../hooks/hook";
31 | import {
32 | useChatDetailsQuery,
33 | useDeleteChatMutation,
34 | useMyGroupsQuery,
35 | useRemoveGroupMemberMutation,
36 | useRenameGroupMutation,
37 | } from "../redux/api/api";
38 | import { setIsAddMember } from "../redux/reducers/misc";
39 |
40 | const ConfirmDeleteDialog = lazy(() =>
41 | import("../components/dialogs/ConfirmDeleteDialog")
42 | );
43 | const AddMemberDialog = lazy(() =>
44 | import("../components/dialogs/AddMemberDialog")
45 | );
46 |
47 | const Groups = () => {
48 | const chatId = useSearchParams()[0].get("group");
49 | const navigate = useNavigate();
50 | const dispatch = useDispatch();
51 |
52 | const { isAddMember } = useSelector((state) => state.misc);
53 |
54 | const myGroups = useMyGroupsQuery("");
55 |
56 | const groupDetails = useChatDetailsQuery(
57 | { chatId, populate: true },
58 | { skip: !chatId }
59 | );
60 |
61 | const [updateGroup, isLoadingGroupName] = useAsyncMutation(
62 | useRenameGroupMutation
63 | );
64 |
65 | const [removeMember, isLoadingRemoveMember] = useAsyncMutation(
66 | useRemoveGroupMemberMutation
67 | );
68 |
69 | const [deleteGroup, isLoadingDeleteGroup] = useAsyncMutation(
70 | useDeleteChatMutation
71 | );
72 |
73 | const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
74 | const [isEdit, setIsEdit] = useState(false);
75 | const [confirmDeleteDialog, setConfirmDeleteDialog] = useState(false);
76 |
77 | const [groupName, setGroupName] = useState("");
78 | const [groupNameUpdatedValue, setGroupNameUpdatedValue] = useState("");
79 |
80 | const [members, setMembers] = useState([]);
81 |
82 | const errors = [
83 | {
84 | isError: myGroups.isError,
85 | error: myGroups.error,
86 | },
87 | {
88 | isError: groupDetails.isError,
89 | error: groupDetails.error,
90 | },
91 | ];
92 |
93 | useErrors(errors);
94 |
95 | useEffect(() => {
96 | const groupData = groupDetails.data;
97 | if (groupData) {
98 | setGroupName(groupData.chat.name);
99 | setGroupNameUpdatedValue(groupData.chat.name);
100 | setMembers(groupData.chat.members);
101 | }
102 |
103 | return () => {
104 | setGroupName("");
105 | setGroupNameUpdatedValue("");
106 | setMembers([]);
107 | setIsEdit(false);
108 | };
109 | }, [groupDetails.data]);
110 |
111 | const navigateBack = () => {
112 | navigate("/");
113 | };
114 |
115 | const handleMobile = () => {
116 | setIsMobileMenuOpen((prev) => !prev);
117 | };
118 |
119 | const handleMobileClose = () => setIsMobileMenuOpen(false);
120 |
121 | const updateGroupName = () => {
122 | setIsEdit(false);
123 | updateGroup("Updating Group Name...", {
124 | chatId,
125 | name: groupNameUpdatedValue,
126 | });
127 | };
128 |
129 | const openConfirmDeleteHandler = () => {
130 | setConfirmDeleteDialog(true);
131 | };
132 |
133 | const closeConfirmDeleteHandler = () => {
134 | setConfirmDeleteDialog(false);
135 | };
136 |
137 | const openAddMemberHandler = () => {
138 | dispatch(setIsAddMember(true));
139 | };
140 |
141 | const deleteHandler = () => {
142 | deleteGroup("Deleting Group...", chatId);
143 | closeConfirmDeleteHandler();
144 | navigate("/groups");
145 | };
146 |
147 | const removeMemberHandler = (userId) => {
148 | removeMember("Removing Member...", { chatId, userId });
149 | };
150 |
151 | useEffect(() => {
152 | if (chatId) {
153 | setGroupName(`Group Name ${chatId}`);
154 | setGroupNameUpdatedValue(`Group Name ${chatId}`);
155 | }
156 |
157 | return () => {
158 | setGroupName("");
159 | setGroupNameUpdatedValue("");
160 | setIsEdit(false);
161 | };
162 | }, [chatId]);
163 |
164 | const IconBtns = (
165 | <>
166 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
196 |
197 |
198 |
199 | >
200 | );
201 |
202 | const GroupName = (
203 |
210 | {isEdit ? (
211 | <>
212 | setGroupNameUpdatedValue(e.target.value)}
215 | />
216 |
217 |
218 |
219 | >
220 | ) : (
221 | <>
222 | {groupName}
223 | setIsEdit(true)}
226 | >
227 |
228 |
229 | >
230 | )}
231 |
232 | );
233 |
234 | const ButtonGroup = (
235 |
247 | }
251 | onClick={openConfirmDeleteHandler}
252 | >
253 | Delete Group
254 |
255 | }
259 | onClick={openAddMemberHandler}
260 | >
261 | Add Member
262 |
263 |
264 | );
265 |
266 | return myGroups.isLoading ? (
267 |
268 | ) : (
269 |
270 |
280 |
281 |
282 |
283 |
295 | {IconBtns}
296 |
297 | {groupName && (
298 | <>
299 | {GroupName}
300 |
301 |
306 | Members
307 |
308 |
309 |
322 | {/* Members */}
323 |
324 | {isLoadingRemoveMember ? (
325 |
326 | ) : (
327 | members.map((i) => (
328 |
339 | ))
340 | )}
341 |
342 |
343 | {ButtonGroup}
344 | >
345 | )}
346 |
347 |
348 | {isAddMember && (
349 | }>
350 |
351 |
352 | )}
353 |
354 | {confirmDeleteDialog && (
355 | }>
356 |
361 |
362 | )}
363 |
364 |
374 |
379 |
380 |
381 | );
382 | };
383 |
384 | const GroupsList = ({ w = "100%", myGroups = [], chatId }) => (
385 |
393 | {myGroups.length > 0 ? (
394 | myGroups.map((group) => (
395 |
396 | ))
397 | ) : (
398 |
399 | No groups
400 |
401 | )}
402 |
403 | );
404 |
405 | const GroupListItem = memo(({ group, chatId }) => {
406 | const { name, avatar, _id } = group;
407 |
408 | return (
409 | {
412 | if (chatId === _id) e.preventDefault();
413 | }}
414 | >
415 |
416 |
417 | {name}
418 |
419 |
420 | );
421 | });
422 |
423 | export default Groups;
424 |
--------------------------------------------------------------------------------