├── .env.example
├── .gitignore
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
└── src
├── App.js
├── assets
├── avatarr.png
├── loader.gif
└── login.svg
├── components
├── card
│ ├── Card.js
│ └── Card.module.scss
├── changeRole
│ └── ChangeRole.js
├── footer
│ └── Footer.js
├── header
│ ├── Header.js
│ └── Header.scss
├── infoBox
│ ├── InfoBox.js
│ └── InfoBox.scss
├── layout
│ └── Layout.js
├── loader
│ ├── Loader.js
│ └── Loader.scss
├── notification
│ ├── Notification.js
│ └── Notification.scss
├── pageMenu
│ └── PageMenu.js
├── passwordInput
│ ├── PasswordInput.js
│ └── PasswordInput.scss
├── protect
│ └── hiddenLink.js
├── search
│ ├── Search.js
│ └── Search.module.scss
└── userStats
│ ├── UserStats.js
│ └── UsersStats.scss
├── customHook
└── useRedirectLoggedOutUser.js
├── index.css
├── index.js
├── pages
├── auth
│ ├── Forgot.js
│ ├── Login.js
│ ├── LoginWithCode.js
│ ├── Register.js
│ ├── Reset.js
│ ├── Verify.js
│ └── auth.module.scss
├── changePassword
│ ├── ChangePassword.js
│ └── ChangePassword.scss
├── home
│ ├── Home.js
│ └── Home.scss
├── profile
│ ├── Profile.js
│ └── Profile.scss
└── userList
│ ├── UserList.js
│ └── UserList.scss
└── redux
├── features
├── auth
│ ├── authService.js
│ ├── authSlice.js
│ └── filterSlice.js
└── email
│ ├── emailService.js
│ └── emailSlice.js
└── store.js
/.env.example:
--------------------------------------------------------------------------------
1 | REACT_APP_BACKEND_URL=http://localhost:5000
2 | REACT_APP_CLOUD_NAME=zinotrust
3 | REACT_APP_UPLOAD_PRESET=sdfvdfgdvd
4 | REACT_APP_GOOGLE_CLIENT_ID=149927346748-tbjbdk61ukaipkflo28g91cokrsrnm64.apps.googleusercontent.com
5 | REACT_APP_GOOGLE_CLIENT_SECRET=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
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .env
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@react-oauth/google": "^0.5.0",
7 | "@reduxjs/toolkit": "^1.9.0",
8 | "@testing-library/jest-dom": "^5.16.5",
9 | "@testing-library/react": "^13.4.0",
10 | "@testing-library/user-event": "^13.5.0",
11 | "axios": "^1.1.3",
12 | "node-sass": "^7.0.3",
13 | "react": "^18.2.0",
14 | "react-confirm-alert": "^3.0.6",
15 | "react-dom": "^18.2.0",
16 | "react-icons": "^4.6.0",
17 | "react-paginate": "^8.1.4",
18 | "react-redux": "^8.0.5",
19 | "react-router-dom": "^6.4.3",
20 | "react-scripts": "5.0.1",
21 | "react-toastify": "^9.1.1",
22 | "web-vitals": "^2.1.4"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": [
32 | "react-app",
33 | "react-app/jest"
34 | ]
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.2%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/mern-auth-frontend/54798b75b25e27d4b3f69b970cc1c847bfc00e7a/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 | React App
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/mern-auth-frontend/54798b75b25e27d4b3f69b970cc1c847bfc00e7a/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/mern-auth-frontend/54798b75b25e27d4b3f69b970cc1c847bfc00e7a/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { BrowserRouter, Routes, Route } from "react-router-dom";
2 | import Layout from "./components/layout/Layout";
3 |
4 | import Forgot from "./pages/auth/Forgot";
5 | import Login from "./pages/auth/Login";
6 | import LoginWithCode from "./pages/auth/LoginWithCode";
7 | import Register from "./pages/auth/Register";
8 | import Reset from "./pages/auth/Reset";
9 | import Verify from "./pages/auth/Verify";
10 | import ChangePassword from "./pages/changePassword/ChangePassword";
11 | import Home from "./pages/home/Home";
12 | import Profile from "./pages/profile/Profile";
13 | import UserList from "./pages/userList/UserList";
14 | import axios from "axios";
15 | import { ToastContainer } from "react-toastify";
16 | import "react-toastify/dist/ReactToastify.css";
17 | import { useEffect } from "react";
18 | import { useDispatch, useSelector } from "react-redux";
19 | import {
20 | getLoginStatus,
21 | getUser,
22 | selectIsLoggedIn,
23 | selectUser,
24 | } from "./redux/features/auth/authSlice";
25 | import { GoogleOAuthProvider } from "@react-oauth/google";
26 |
27 | axios.defaults.withCredentials = true;
28 |
29 | function App() {
30 | const dispatch = useDispatch();
31 | const isLoggedIn = useSelector(selectIsLoggedIn);
32 | const user = useSelector(selectUser);
33 |
34 | useEffect(() => {
35 | dispatch(getLoginStatus());
36 | if (isLoggedIn && user === null) {
37 | dispatch(getUser());
38 | }
39 | }, [dispatch, isLoggedIn, user]);
40 |
41 | return (
42 | <>
43 |
44 |
45 |
46 |
47 |
51 |
52 |
53 | }
54 | />
55 | } />
56 | } />
57 | } />
58 | } />
59 | } />
60 |
61 |
65 |
66 |
67 | }
68 | />
69 |
73 |
74 |
75 | }
76 | />
77 |
81 |
82 |
83 | }
84 | />
85 |
89 |
90 |
91 | }
92 | />
93 |
94 |
95 |
96 | >
97 | );
98 | }
99 |
100 | export default App;
101 |
--------------------------------------------------------------------------------
/src/assets/avatarr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/mern-auth-frontend/54798b75b25e27d4b3f69b970cc1c847bfc00e7a/src/assets/avatarr.png
--------------------------------------------------------------------------------
/src/assets/loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zinotrust/mern-auth-frontend/54798b75b25e27d4b3f69b970cc1c847bfc00e7a/src/assets/loader.gif
--------------------------------------------------------------------------------
/src/assets/login.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/card/Card.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Card.module.scss";
3 |
4 | const Card = ({ children, cardClass }) => {
5 | return {children}
;
6 | };
7 |
8 | export default Card;
9 |
--------------------------------------------------------------------------------
/src/components/card/Card.module.scss:
--------------------------------------------------------------------------------
1 | .card {
2 | border: 1px solid transparent;
3 | border-radius: 5px;
4 | box-shadow: var(--box-shadow);
5 | overflow: hidden;
6 | }
7 |
--------------------------------------------------------------------------------
/src/components/changeRole/ChangeRole.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FaCheck } from "react-icons/fa";
3 | import { useDispatch } from "react-redux";
4 | import { toast } from "react-toastify";
5 | import { getUsers, upgradeUser } from "../../redux/features/auth/authSlice";
6 | import {
7 | EMAIL_RESET,
8 | sendAutomatedEmail,
9 | } from "../../redux/features/email/emailSlice";
10 |
11 | const ChangeRole = ({ _id, email }) => {
12 | const [userRole, setUserRole] = useState("");
13 | const dispatch = useDispatch();
14 |
15 | // Change User role
16 | const changeRole = async (e) => {
17 | e.preventDefault();
18 |
19 | if (!userRole) {
20 | toast.error("Please select a role");
21 | }
22 |
23 | const userData = {
24 | role: userRole,
25 | id: _id,
26 | };
27 |
28 | const emailData = {
29 | subject: "Account Role Changed - AUTH:Z",
30 | send_to: email,
31 | reply_to: "noreply@zino",
32 | template: "changeRole",
33 | url: "/login",
34 | };
35 |
36 | await dispatch(upgradeUser(userData));
37 | await dispatch(sendAutomatedEmail(emailData));
38 | await dispatch(getUsers());
39 | dispatch(EMAIL_RESET());
40 | };
41 |
42 | return (
43 |
44 |
59 |
60 | );
61 | };
62 |
63 | export default ChangeRole;
64 |
--------------------------------------------------------------------------------
/src/components/footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Footer = () => {
4 | return (
5 | <>
6 |
7 |
8 |
All Rights Reserved. © 2023
9 |
10 | >
11 | );
12 | };
13 |
14 | export default Footer;
15 |
--------------------------------------------------------------------------------
/src/components/header/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Header.scss";
3 | import { BiLogIn } from "react-icons/bi";
4 | import { FaUserCircle } from "react-icons/fa";
5 | import { Link, NavLink, useNavigate } from "react-router-dom";
6 | import { useDispatch } from "react-redux";
7 | import { logout, RESET } from "../../redux/features/auth/authSlice";
8 | import { ShowOnLogin, ShowOnLogout } from "../protect/hiddenLink";
9 | import { UserName } from "../../pages/profile/Profile";
10 |
11 | const activeLink = ({ isActive }) => (isActive ? "active" : "");
12 |
13 | const Header = () => {
14 | const navigate = useNavigate();
15 | const dispatch = useDispatch();
16 |
17 | const goHome = () => {
18 | navigate("/");
19 | };
20 |
21 | const logoutUser = async () => {
22 | dispatch(RESET());
23 | await dispatch(logout());
24 | navigate("/login");
25 | };
26 |
27 | return (
28 |
29 |
63 |
64 | );
65 | };
66 |
67 | export default Header;
68 |
--------------------------------------------------------------------------------
/src/components/header/Header.scss:
--------------------------------------------------------------------------------
1 | .header {
2 | background-color: #030b6b;
3 | height: 7rem;
4 |
5 | display: flex;
6 | justify-content: center;
7 | align-items: center;
8 | // max-width: 1000px;
9 | width: 100%;
10 | margin: 0 auto;
11 | padding: 0 20px;
12 |
13 | & nav {
14 | width: 100%;
15 | max-width: 1000px;
16 | color: #fff;
17 | display: flex;
18 | justify-content: space-between;
19 | align-items: center;
20 |
21 | & > .logo {
22 | color: #fff;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | font-size: 3rem;
27 | cursor: pointer;
28 | }
29 | }
30 | }
31 | .home-links {
32 | display: flex;
33 | justify-content: center;
34 | align-items: center;
35 | & li a {
36 | color: #fff;
37 | }
38 | & > * {
39 | margin-left: 1rem;
40 | color: #fff;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/infoBox/InfoBox.js:
--------------------------------------------------------------------------------
1 | import "./InfoBox.scss";
2 |
3 | const InfoBox = ({ bgColor, title, count, icon }) => {
4 | return (
5 |
6 |
{icon}
7 |
8 | {title}
9 | {count}
10 |
11 |
12 | );
13 | };
14 |
15 | export default InfoBox;
16 |
--------------------------------------------------------------------------------
/src/components/infoBox/InfoBox.scss:
--------------------------------------------------------------------------------
1 | .info-box {
2 | width: 100%;
3 | height: 7rem;
4 | max-width: 22rem;
5 | margin-right: 1rem;
6 | margin-bottom: 1rem;
7 | display: flex;
8 | justify-content: flex-start;
9 | align-items: center;
10 | vertical-align: middle;
11 | flex-wrap: wrap;
12 | color: #fff;
13 | transform: translateY(0);
14 | transition: all 0.3s;
15 |
16 | // background-color: green;
17 |
18 | &:hover {
19 | cursor: pointer;
20 | // border: 3px solid #fff;
21 | transform: translateY(-5px);
22 | }
23 |
24 | .info-icon {
25 | padding: 0 2rem;
26 | color: #fff;
27 | }
28 | .info-text > * {
29 | color: #fff;
30 | }
31 |
32 | // .card {
33 | // border: 1px solid #ccc;
34 | // border-bottom: 3px solid var(--light-blue);
35 | // padding: 5px;
36 | // background-color: #f5f6fa;
37 | // }
38 | }
39 |
40 | @media screen and (max-width: 600px) {
41 | .info-box {
42 | max-width: 100%;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/layout/Layout.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Footer from "../footer/Footer";
3 | import Header from "../header/Header";
4 |
5 | const Layout = ({ children }) => {
6 | return (
7 | <>
8 |
9 |
10 | {children}
11 |
12 |
13 | >
14 | );
15 | };
16 |
17 | export default Layout;
18 |
--------------------------------------------------------------------------------
/src/components/loader/Loader.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Loader.scss";
3 | import ReactDOM from "react-dom";
4 | import loaderImg from "../../assets/loader.gif";
5 |
6 | const Loader = () => {
7 | return ReactDOM.createPortal(
8 |
9 |
10 |

11 |
12 |
,
13 | document.getElementById("loader")
14 | );
15 | };
16 |
17 | export const Spinner = () => {
18 | return (
19 |
20 |

21 |
22 | );
23 | };
24 |
25 | export default Loader;
26 |
--------------------------------------------------------------------------------
/src/components/loader/Loader.scss:
--------------------------------------------------------------------------------
1 | /* Loading */
2 | .wrapper {
3 | position: fixed;
4 | width: 100vw;
5 | height: 100vh;
6 | background-color: rgba(0, 0, 0, 0.7);
7 | z-index: 9;
8 | }
9 |
10 | .loader {
11 | position: fixed;
12 | left: 50%;
13 | top: 50%;
14 | transform: translate(-50%, -50%);
15 | z-index: 999;
16 | }
17 |
--------------------------------------------------------------------------------
/src/components/notification/Notification.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import {
4 | RESET,
5 | sendVerificationEmail,
6 | } from "../../redux/features/auth/authSlice";
7 | import "./Notification.scss";
8 |
9 | const Notification = () => {
10 | const dispatch = useDispatch();
11 |
12 | const sendVerEmail = async () => {
13 | await dispatch(sendVerificationEmail());
14 | await dispatch(RESET());
15 | };
16 |
17 | return (
18 |
19 |
20 |
21 | Message:
22 |
23 |
24 | To verify your account, check your email for a verification link.
25 |
26 |
27 |
28 | Resend Link
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default Notification;
36 |
--------------------------------------------------------------------------------
/src/components/notification/Notification.scss:
--------------------------------------------------------------------------------
1 | .alert {
2 | width: 100%;
3 | border: 1px solid rgb(232, 72, 72);
4 | border-radius: 3px;
5 | background-color: rgba(222, 31, 31, 0.2);
6 | position: relative;
7 | display: flex;
8 | justify-content: flex-start;
9 | padding: 1rem;
10 | }
11 |
12 | .v-link {
13 | cursor: pointer;
14 | color: var(--light-blue);
15 | }
16 |
17 | .alert p {
18 | font-size: 1.3rem;
19 | }
20 | @media screen and (max-width: 600px) {
21 | .alert p {
22 | font-size: 1.2rem;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/components/pageMenu/PageMenu.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NavLink } from "react-router-dom";
3 | import { AdminAuthorLink } from "../protect/hiddenLink";
4 |
5 | const PageMenu = () => {
6 | return (
7 |
8 |
23 |
24 | );
25 | };
26 |
27 | export default PageMenu;
28 |
--------------------------------------------------------------------------------
/src/components/passwordInput/PasswordInput.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import "./PasswordInput.scss";
3 | import { AiOutlineEye, AiOutlineEyeInvisible } from "react-icons/ai";
4 |
5 | const PasswordInput = ({ placeholder, value, onChange, name, onPaste }) => {
6 | const [showPassword, setShowPassword] = useState(false);
7 |
8 | const togglePassword = () => {
9 | setShowPassword(!showPassword);
10 | };
11 | return (
12 |
13 |
22 |
23 | {showPassword ? (
24 |
25 | ) : (
26 |
27 | )}
28 |
29 |
30 | );
31 | };
32 |
33 | export default PasswordInput;
34 |
--------------------------------------------------------------------------------
/src/components/passwordInput/PasswordInput.scss:
--------------------------------------------------------------------------------
1 | .password {
2 | position: relative;
3 |
4 | .icon {
5 | position: absolute;
6 | top: 1rem;
7 | right: 1rem;
8 | cursor: pointer;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/components/protect/hiddenLink.js:
--------------------------------------------------------------------------------
1 | const { useSelector } = require("react-redux");
2 | const {
3 | selectIsLoggedIn,
4 | selectUser,
5 | } = require("../../redux/features/auth/authSlice");
6 |
7 | export const ShowOnLogin = ({ children }) => {
8 | const isLoggedIn = useSelector(selectIsLoggedIn);
9 |
10 | if (isLoggedIn) {
11 | return <>{children}>;
12 | }
13 | return null;
14 | };
15 |
16 | export const ShowOnLogout = ({ children }) => {
17 | const isLoggedIn = useSelector(selectIsLoggedIn);
18 |
19 | if (!isLoggedIn) {
20 | return <>{children}>;
21 | }
22 | return null;
23 | };
24 |
25 | export const AdminAuthorLink = ({ children }) => {
26 | const isLoggedIn = useSelector(selectIsLoggedIn);
27 | const user = useSelector(selectUser);
28 |
29 | if (isLoggedIn && (user?.role === "admin" || user?.role === "author")) {
30 | return <>{children}>;
31 | }
32 | return null;
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/search/Search.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BiSearch } from "react-icons/bi";
3 | import styles from "./Search.module.scss";
4 |
5 | const Search = ({ value, onChange }) => {
6 | return (
7 |
8 |
9 |
15 |
16 | );
17 | };
18 |
19 | export default Search;
20 |
--------------------------------------------------------------------------------
/src/components/search/Search.module.scss:
--------------------------------------------------------------------------------
1 | .search {
2 | margin: 5px 0;
3 | position: relative;
4 | flex: 1;
5 |
6 | .icon {
7 | position: absolute;
8 | top: 50%;
9 | left: 1rem;
10 | transform: translateY(-50%);
11 | }
12 |
13 | input[type="text"] {
14 | display: block;
15 | font-size: 1.6rem;
16 | font-weight: 300;
17 | padding: 1rem;
18 | padding-left: 3rem;
19 | margin: 1rem auto;
20 | width: 100%;
21 | border: 1px solid #777;
22 | border-radius: 3px;
23 | outline: none;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/userStats/UserStats.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { BiUserCheck, BiUserMinus, BiUserX } from "react-icons/bi";
3 | import { FaUsers } from "react-icons/fa";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import {
6 | CALC_SUSPENDED_USER,
7 | CALC_VERIFIED_USER,
8 | } from "../../redux/features/auth/authSlice";
9 | import InfoBox from "../infoBox/InfoBox";
10 | import "./UsersStats.scss";
11 |
12 | // Icons
13 | const icon1 = ;
14 | const icon2 = ;
15 | const icon3 = ;
16 | const icon4 = ;
17 |
18 | const UserStats = () => {
19 | const dispatch = useDispatch();
20 | const { users, verifiedUsers, suspendedUsers } = useSelector(
21 | (state) => state.auth
22 | );
23 | const unverifiedUsers = users.length - verifiedUsers;
24 |
25 | useEffect(() => {
26 | dispatch(CALC_VERIFIED_USER());
27 | dispatch(CALC_SUSPENDED_USER());
28 | }, [dispatch, users]);
29 |
30 | return (
31 |
32 |
User Stats
33 |
34 |
40 |
46 |
52 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default UserStats;
64 |
--------------------------------------------------------------------------------
/src/components/userStats/UsersStats.scss:
--------------------------------------------------------------------------------
1 | .user-summary {
2 | width: 100%;
3 | .info-summary {
4 | display: flex;
5 | flex-wrap: wrap;
6 | }
7 | }
8 |
9 | .card {
10 | border: 1px solid #ccc;
11 | // border-bottom: 3px solid var(--light-blue);
12 | padding: 5px;
13 | background-color: #f5f6fa;
14 | }
15 |
16 | .card1 {
17 | background-color: #b624ff;
18 | }
19 | .card2 {
20 | background-color: #32963d;
21 | }
22 | .card3 {
23 | background-color: #03a5fc;
24 | }
25 | .card4 {
26 | background-color: #c41849;
27 | }
28 |
--------------------------------------------------------------------------------
/src/customHook/useRedirectLoggedOutUser.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { useNavigate } from "react-router-dom";
3 | import { toast } from "react-toastify";
4 | import authService from "../redux/features/auth/authService";
5 |
6 | const useRedirectLoggedOutUser = (path) => {
7 | const navigate = useNavigate();
8 |
9 | useEffect(() => {
10 | let isLoggedIn;
11 | const redirectLoggedOutUser = async () => {
12 | try {
13 | isLoggedIn = await authService.getLoginStatus();
14 | } catch (error) {
15 | console.log(error.message);
16 | }
17 |
18 | if (!isLoggedIn) {
19 | toast.info("Session expired, please login to continue");
20 | navigate(path);
21 | return;
22 | }
23 | };
24 | redirectLoggedOutUser();
25 | }, [path, navigate]);
26 | };
27 |
28 | export default useRedirectLoggedOutUser;
29 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@100;200;300;400;500;600;700;800;900&display=swap");
2 |
3 | :root {
4 | --nav-width: 68px;
5 | --header-height: 45px;
6 |
7 | --font-family: "Poppins", sans-serif;
8 | --dark-blue: #0a1930;
9 | --light-blue: #1f93ff;
10 |
11 | --color-white: #fff;
12 | --color-dark: #333;
13 |
14 | --color-grey: #eee;
15 | --box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
16 |
17 | --color-purple: #9d0191;
18 | --color-orange: #ff7722;
19 |
20 | --color-primary: #007bff;
21 | --color-success: #28a745;
22 | --color-danger: orangered;
23 | }
24 |
25 | * {
26 | margin: 0;
27 | padding: 0;
28 | box-sizing: border-box;
29 | scroll-behavior: smooth;
30 | }
31 |
32 | html {
33 | font-size: 10px;
34 | }
35 |
36 | body {
37 | font-family: var(--font-family);
38 | background: #eee;
39 | }
40 |
41 | section {
42 | width: 100%;
43 | padding: 4rem 0;
44 | }
45 |
46 | .container {
47 | max-width: 1000px;
48 | margin: 0 auto;
49 | padding: 0 20px;
50 | }
51 |
52 | .--pad {
53 | max-width: 100%;
54 | margin: 0 auto;
55 | padding: 0 20px;
56 | }
57 |
58 | .header {
59 | padding-top: 2rem;
60 | }
61 |
62 | /* UTILITY CLASSES */
63 |
64 | /* Flex */
65 | .--flex-center {
66 | display: flex;
67 | justify-content: center;
68 | align-items: center;
69 | }
70 | .--flex-start {
71 | display: flex;
72 | justify-content: flex-start;
73 | align-items: flex-start;
74 | }
75 | .--flex-end {
76 | display: flex;
77 | justify-content: flex-end;
78 | align-items: center;
79 | }
80 | .--flex-between {
81 | display: flex;
82 | justify-content: space-between;
83 | align-items: center;
84 | }
85 |
86 | .--dir-column {
87 | flex-direction: column;
88 | }
89 |
90 | .--flex-dir-column {
91 | display: flex;
92 | }
93 |
94 | .--align-center {
95 | display: flex;
96 | align-items: center;
97 | }
98 |
99 | .--100vh {
100 | height: 100vh;
101 | }
102 |
103 | .--mh-100vh {
104 | min-height: 100vh;
105 | }
106 |
107 | /* Grid */
108 | .--grid-15 {
109 | display: grid;
110 | grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
111 | row-gap: 1rem;
112 | column-gap: 1rem;
113 | }
114 | .--grid-25 {
115 | display: grid;
116 | grid-template-columns: repeat(auto-fit, minmax(24rem, 1fr));
117 | row-gap: 1rem;
118 | column-gap: 1rem;
119 | }
120 |
121 | /* Center All */
122 | .--center-all {
123 | display: flex;
124 | justify-content: center;
125 | align-items: center;
126 | flex-direction: column;
127 | width: 100%;
128 | margin: auto;
129 | text-align: center;
130 | }
131 |
132 | /* Heading */
133 | h1,
134 | h2,
135 | h3,
136 | h4 {
137 | font-weight: 500;
138 | line-height: 1.2;
139 | color: var(--color-dark);
140 | margin-bottom: 1rem;
141 | }
142 | h1 {
143 | font-size: 4rem;
144 | }
145 | h2 {
146 | font-size: 3rem;
147 | }
148 | h3 {
149 | font-size: 2.5rem;
150 | }
151 | h4 {
152 | font-size: 2rem;
153 | }
154 |
155 | p {
156 | font-size: 1.5rem;
157 | font-weight: 300;
158 | line-height: 1.3;
159 | color: var(--color-dark);
160 | }
161 | .--text-xl {
162 | font-size: 4.5rem;
163 | }
164 | .--text-lg {
165 | font-size: 4rem;
166 | }
167 |
168 | .--text-md {
169 | font-size: 3rem;
170 | }
171 |
172 | .--text-sm {
173 | font-size: 1.2rem;
174 | font-weight: 300;
175 | }
176 |
177 | .--text-p {
178 | font-size: 1.5rem;
179 | font-weight: 300;
180 | line-height: 1.3;
181 | color: var(--color-dark);
182 | }
183 |
184 | .--fw-bold {
185 | font-weight: 600;
186 | }
187 | .--fw-thin {
188 | font-weight: 200;
189 | }
190 |
191 | /* Text Color */
192 | .--text-light {
193 | color: #fff;
194 | }
195 |
196 | .--color-primary {
197 | color: #007bff;
198 | }
199 | .--color-danger {
200 | color: orangered;
201 | }
202 | .--color-success {
203 | color: #28a745;
204 | }
205 |
206 | .--color-white {
207 | color: #fff;
208 | }
209 | .--color-dark {
210 | color: #333;
211 | }
212 |
213 | /* Center Text */
214 | .--text-center {
215 | text-align: center;
216 | }
217 |
218 | /* Card */
219 | .--card {
220 | border: 1px solid transparent;
221 | border-radius: 5px;
222 | box-shadow: var(--box-shadow);
223 | overflow: hidden;
224 | }
225 |
226 | /* Margin */
227 | .--m {
228 | margin: 1rem;
229 | }
230 | .--ml {
231 | margin-left: 1rem;
232 | }
233 | .--mr {
234 | margin-right: 1rem;
235 | }
236 |
237 | .--mb {
238 | margin-bottom: 1rem;
239 | }
240 | .--mt {
241 | margin-top: 1rem;
242 | }
243 |
244 | .--my {
245 | margin: 1rem 0;
246 | }
247 | .--mx {
248 | margin: 0 1rem;
249 | }
250 |
251 | .--m2 {
252 | margin: 2rem;
253 | }
254 |
255 | .--ml2 {
256 | margin-left: 2rem;
257 | }
258 | .--mr2 {
259 | margin-right: 2rem;
260 | }
261 |
262 | .--mb2 {
263 | margin-bottom: 2rem;
264 | }
265 |
266 | .--my2 {
267 | margin: 2rem 0;
268 | }
269 |
270 | .--mx2 {
271 | margin: 0 2rem;
272 | }
273 |
274 | /* Padding */
275 | .--p {
276 | padding: 1rem;
277 | }
278 | .--p2 {
279 | padding: 2rem;
280 | }
281 | .--py {
282 | padding: 1rem 0;
283 | }
284 | .--py2 {
285 | padding: 2rem 0;
286 | }
287 | .--px {
288 | padding: 0 1rem;
289 | }
290 | .--px2 {
291 | padding: 0 2rem;
292 | }
293 |
294 | .--btn {
295 | font-size: 1.6rem;
296 | font-weight: 400;
297 | padding: 6px 8px;
298 | margin: 0 5px 0 0;
299 | border: 1px solid transparent;
300 | border-radius: 3px;
301 | cursor: pointer;
302 | display: flex;
303 | justify-content: center;
304 | align-items: center;
305 | transition: all 0.3s;
306 | }
307 |
308 | .--btn:hover {
309 | transform: translateY(-2px);
310 | }
311 |
312 | .--btn-lg {
313 | padding: 8px 10px;
314 | }
315 |
316 | .--btn-block {
317 | width: 100%;
318 | }
319 |
320 | .--btn-primary {
321 | color: #fff;
322 | background: #007bff;
323 | }
324 | .--btn-secondary {
325 | color: #fff;
326 | border: 1px solid #fff;
327 | background: transparent;
328 | }
329 | .--btn-danger {
330 | color: #fff;
331 | background: orangered;
332 | }
333 | .--btn-google {
334 | color: #fff;
335 | background: rgb(174, 49, 4);
336 | }
337 |
338 | .--btn-success {
339 | color: #fff;
340 | background: #28a745;
341 | }
342 |
343 | /* Background */
344 | .--bg-light {
345 | background: #fff;
346 | }
347 | .--bg-dark {
348 | background: var(--color-dark);
349 | }
350 | .--bg-primary {
351 | background: var(--color-primary);
352 | }
353 | .--bg-success {
354 | background: var(--color-success);
355 | }
356 | .--bg-grey {
357 | background: var(--color-grey);
358 | }
359 |
360 | .--form-control {
361 | font-size: 1.6rem;
362 | font-weight: 300;
363 | }
364 |
365 | .--form-control > * {
366 | margin: 5px 0;
367 | }
368 |
369 | .--form-control input {
370 | font-size: 1.6rem;
371 | font-weight: 300;
372 | padding: 8px 1rem;
373 | border: 1px solid #777;
374 | border-radius: 3px;
375 | outline: none;
376 | display: block;
377 | }
378 | .--form-control textarea {
379 | font-size: 1.6rem;
380 | font-weight: 300;
381 | padding: 8px 1rem;
382 | border: 1px solid #777;
383 | border-radius: 3px;
384 | outline: none;
385 | }
386 | .--form-control select {
387 | font-size: 1.4rem;
388 | font-weight: 400;
389 | padding: 8px 1rem;
390 | border: 1px solid #777;
391 | border-radius: 3px;
392 | }
393 |
394 | .--form-control label {
395 | font-size: 1.6rem;
396 | font-weight: 400;
397 | display: inline-block;
398 | min-width: 7rem;
399 | color: var(--color-dark);
400 | margin-right: 1rem;
401 | }
402 |
403 | @media screen and (max-width: 600px) {
404 | .--flex-dir-column {
405 | flex-direction: column;
406 | }
407 | }
408 |
409 | .--block {
410 | display: block;
411 | }
412 | .--inline-block {
413 | display: inline-block;
414 | }
415 |
416 | .--width-100 {
417 | width: 100%;
418 | }
419 |
420 | .--width-500px {
421 | width: 500px;
422 | }
423 |
424 | .--line {
425 | position: relative;
426 | }
427 | .--line::after {
428 | content: "";
429 | position: absolute;
430 | left: 50%;
431 | bottom: -5px;
432 | transform: translateX(-50%);
433 | width: 5rem;
434 | height: 3px;
435 | margin-bottom: 1rem;
436 |
437 | background: rgb(217, 8, 170);
438 | background: linear-gradient(
439 | 135deg,
440 | rgba(163, 1, 191, 1) 44%,
441 | rgba(217, 8, 170, 1) 57%
442 | );
443 | }
444 |
445 | .--list-style-none {
446 | list-style: none;
447 | }
448 |
449 | .--profile-img {
450 | width: 6rem;
451 | height: 6rem;
452 | border: 1px solid #ccc;
453 | border-radius: 50%;
454 | }
455 |
456 | a {
457 | font-size: 1.4rem;
458 | /* color: var(--dark-blue); */
459 | text-decoration: none;
460 | transition: all 0.2s;
461 | }
462 |
463 | ul {
464 | list-style: none;
465 | }
466 |
467 | a:hover {
468 | /* color: var(--color-dark); */
469 | font-size: 1.5rem;
470 | }
471 |
472 | .ql-editor {
473 | min-height: 200px;
474 | }
475 |
476 | .pagination {
477 | list-style: none;
478 | display: flex;
479 | justify-content: center;
480 | align-items: center;
481 | margin: 2rem 0;
482 | font-size: 1rem;
483 | }
484 |
485 | .pagination .page-num {
486 | padding: 5px 10px;
487 | cursor: pointer;
488 | border-radius: 3px;
489 | font-weight: normal;
490 | color: #333;
491 | border: 1px solid #333;
492 | margin: 2px;
493 | }
494 |
495 | .pagination .page-num:hover {
496 | color: #fff;
497 | background-color: var(--light-blue);
498 | }
499 | .activePage {
500 | color: #fff;
501 | background-color: var(--light-blue);
502 | height: 100%;
503 | }
504 |
505 | .active {
506 | position: relative;
507 | color: var(--color-danger);
508 | }
509 |
510 | .active::after {
511 | content: "";
512 | position: absolute;
513 | left: 0;
514 | bottom: -3px;
515 | width: 100%;
516 | height: 2px;
517 | background-color: #fff;
518 | }
519 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import App from "./App";
5 | import { Provider } from "react-redux";
6 | import { store } from "./redux/store";
7 |
8 | const root = ReactDOM.createRoot(document.getElementById("root"));
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/pages/auth/Forgot.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { AiOutlineMail } from "react-icons/ai";
3 | import { BiLogIn } from "react-icons/bi";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { Link } from "react-router-dom";
6 | import { toast } from "react-toastify";
7 | import Card from "../../components/card/Card";
8 | import Loader from "../../components/loader/Loader";
9 | import PasswordInput from "../../components/passwordInput/PasswordInput";
10 | import { validateEmail } from "../../redux/features/auth/authService";
11 | import { forgotPassword, RESET } from "../../redux/features/auth/authSlice";
12 | import styles from "./auth.module.scss";
13 |
14 | const Forgot = () => {
15 | const [email, setEmail] = useState("");
16 | const dispatch = useDispatch();
17 |
18 | const { isLoading } = useSelector((state) => state.auth);
19 |
20 | const forgot = async (e) => {
21 | e.preventDefault();
22 |
23 | if (!email) {
24 | return toast.error("Please enter an email");
25 | }
26 |
27 | if (!validateEmail(email)) {
28 | return toast.error("Please enter a valid email");
29 | }
30 |
31 | const userData = {
32 | email,
33 | };
34 |
35 | await dispatch(forgotPassword(userData));
36 | await dispatch(RESET(userData));
37 | };
38 |
39 | return (
40 |
41 | {isLoading &&
}
42 |
43 |
44 |
47 |
Forgot Password
48 |
49 |
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default Forgot;
78 |
--------------------------------------------------------------------------------
/src/pages/auth/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { BiLogIn } from "react-icons/bi";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { Link, useNavigate } from "react-router-dom";
5 | import { toast } from "react-toastify";
6 | import Card from "../../components/card/Card";
7 | import Loader from "../../components/loader/Loader";
8 | import PasswordInput from "../../components/passwordInput/PasswordInput";
9 | import { validateEmail } from "../../redux/features/auth/authService";
10 | import {
11 | login,
12 | loginWithGoogle,
13 | RESET,
14 | sendLoginCode,
15 | } from "../../redux/features/auth/authSlice";
16 | import styles from "./auth.module.scss";
17 | import { GoogleLogin } from "@react-oauth/google";
18 |
19 | const initialState = {
20 | email: "",
21 | password: "",
22 | };
23 |
24 | const Login = () => {
25 | const [formData, setFormData] = useState(initialState);
26 | const { email, password } = formData;
27 |
28 | const handleInputChange = (e) => {
29 | const { name, value } = e.target;
30 | setFormData({ ...formData, [name]: value });
31 | };
32 | const dispatch = useDispatch();
33 | const navigate = useNavigate();
34 |
35 | const { isLoading, isLoggedIn, isSuccess, message, isError, twoFactor } =
36 | useSelector((state) => state.auth);
37 |
38 | const loginUser = async (e) => {
39 | e.preventDefault();
40 |
41 | if (!email || !password) {
42 | return toast.error("All fields are required");
43 | }
44 |
45 | if (!validateEmail(email)) {
46 | return toast.error("Please enter a valid email");
47 | }
48 |
49 | const userData = {
50 | email,
51 | password,
52 | };
53 |
54 | // console.log(userData);
55 | await dispatch(login(userData));
56 | };
57 |
58 | useEffect(() => {
59 | if (isSuccess && isLoggedIn) {
60 | navigate("/profile");
61 | }
62 |
63 | if (isError && twoFactor) {
64 | dispatch(sendLoginCode(email));
65 | navigate(`/loginWithCode/${email}`);
66 | }
67 |
68 | dispatch(RESET());
69 | }, [isLoggedIn, isSuccess, dispatch, navigate, isError, twoFactor, email]);
70 |
71 | const googleLogin = async (credentialResponse) => {
72 | console.log(credentialResponse);
73 | await dispatch(
74 | loginWithGoogle({ userToken: credentialResponse.credential })
75 | );
76 | };
77 |
78 | return (
79 |
80 | {isLoading &&
}
81 |
82 |
83 |
84 |
85 |
86 |
Login
87 |
88 | {/* */}
90 | {
93 | console.log("Login Failed");
94 | toast.error("Login Failed");
95 | }}
96 | />
97 |
98 |
99 |
or
100 |
101 |
121 |
Forgot Password
122 |
123 | Home
124 | Don't have an account?
125 | Register
126 |
127 |
128 |
129 |
130 | );
131 | };
132 |
133 | export default Login;
134 |
--------------------------------------------------------------------------------
/src/pages/auth/LoginWithCode.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { GrInsecure } from "react-icons/gr";
3 | import { useDispatch, useSelector } from "react-redux";
4 |
5 | import { Link, useNavigate, useParams } from "react-router-dom";
6 | import { toast } from "react-toastify";
7 | import Card from "../../components/card/Card";
8 | import Loader from "../../components/loader/Loader";
9 | import PasswordInput from "../../components/passwordInput/PasswordInput";
10 | import {
11 | loginWithCode,
12 | RESET,
13 | sendLoginCode,
14 | } from "../../redux/features/auth/authSlice";
15 | import styles from "./auth.module.scss";
16 |
17 | const LoginWithCode = () => {
18 | const [loginCode, setLoginCode] = useState("");
19 | const { email } = useParams();
20 | const dispatch = useDispatch();
21 | const navigate = useNavigate();
22 |
23 | const { isLoading, isLoggedIn, isSuccess } = useSelector(
24 | (state) => state.auth
25 | );
26 |
27 | const sendUserLoginCode = async () => {
28 | await dispatch(sendLoginCode(email));
29 | await dispatch(RESET());
30 | };
31 |
32 | const loginUserWithCode = async (e) => {
33 | e.preventDefault();
34 |
35 | if (loginCode === "") {
36 | return toast.error("Please fill in the login code");
37 | }
38 | if (loginCode.length !== 6) {
39 | return toast.error("Access code must be 6 characters");
40 | }
41 |
42 | const code = {
43 | loginCode,
44 | };
45 |
46 | await dispatch(loginWithCode({ code, email }));
47 | };
48 |
49 | useEffect(() => {
50 | if (isSuccess && isLoggedIn) {
51 | navigate("/profile");
52 | }
53 |
54 | dispatch(RESET());
55 | }, [isLoggedIn, isSuccess, dispatch, navigate]);
56 |
57 | return (
58 |
59 | {isLoading &&
}
60 |
61 |
62 |
63 |
64 |
65 |
Enter Access Code
66 |
67 |
92 |
93 |
94 |
95 | );
96 | };
97 |
98 | export default LoginWithCode;
99 |
--------------------------------------------------------------------------------
/src/pages/auth/Register.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { FaTimes } from "react-icons/fa";
3 | import { BsCheck2All } from "react-icons/bs";
4 | import { TiUserAddOutline } from "react-icons/ti";
5 | import { Link, useNavigate } from "react-router-dom";
6 | import Card from "../../components/card/Card";
7 | import PasswordInput from "../../components/passwordInput/PasswordInput";
8 | import styles from "./auth.module.scss";
9 | import { toast } from "react-toastify";
10 | import { validateEmail } from "../../redux/features/auth/authService";
11 | import { useDispatch, useSelector } from "react-redux";
12 | import {
13 | register,
14 | RESET,
15 | sendVerificationEmail,
16 | } from "../../redux/features/auth/authSlice";
17 | import Loader from "../../components/loader/Loader";
18 |
19 | const initialState = {
20 | name: "",
21 | email: "",
22 | password: "",
23 | password2: "",
24 | };
25 |
26 | const Register = () => {
27 | const [formData, setFormData] = useState(initialState);
28 | const { name, email, password, password2 } = formData;
29 |
30 | const dispatch = useDispatch();
31 | const navigate = useNavigate();
32 |
33 | const { isLoading, isLoggedIn, isSuccess, message } = useSelector(
34 | (state) => state.auth
35 | );
36 |
37 | const [uCase, setUCase] = useState(false);
38 | const [num, setNum] = useState(false);
39 | const [sChar, setSChar] = useState(false);
40 | const [passLength, setPassLength] = useState(false);
41 |
42 | const timesIcon = ;
43 | const checkIcon = ;
44 |
45 | const switchIcon = (condition) => {
46 | if (condition) {
47 | return checkIcon;
48 | }
49 | return timesIcon;
50 | };
51 |
52 | const handleInputChange = (e) => {
53 | const { name, value } = e.target;
54 | setFormData({ ...formData, [name]: value });
55 | };
56 |
57 | useEffect(() => {
58 | // Check Lower and Uppercase
59 | if (password.match(/([a-z].*[A-Z])|([A-Z].*[a-z])/)) {
60 | setUCase(true);
61 | } else {
62 | setUCase(false);
63 | }
64 | // Check for numbers
65 | if (password.match(/([0-9])/)) {
66 | setNum(true);
67 | } else {
68 | setNum(false);
69 | }
70 | // Check for special character
71 | if (password.match(/([!,%,&,@,#,$,^,*,?,_,~])/)) {
72 | setSChar(true);
73 | } else {
74 | setSChar(false);
75 | }
76 | // Check for PASSWORD LENGTH
77 | if (password.length > 5) {
78 | setPassLength(true);
79 | } else {
80 | setPassLength(false);
81 | }
82 | }, [password]);
83 |
84 | const registerUser = async (e) => {
85 | e.preventDefault();
86 |
87 | if (!name || !email || !password) {
88 | return toast.error("All fields are required");
89 | }
90 | if (password.length < 6) {
91 | return toast.error("Password must be up to 6 characters");
92 | }
93 | if (!validateEmail(email)) {
94 | return toast.error("Please enter a valid email");
95 | }
96 | if (password !== password2) {
97 | return toast.error("Passwords do not match");
98 | }
99 |
100 | const userData = {
101 | name,
102 | email,
103 | password,
104 | };
105 |
106 | // console.log(userData);
107 | await dispatch(register(userData));
108 | await dispatch(sendVerificationEmail());
109 | };
110 |
111 | useEffect(() => {
112 | if (isSuccess && isLoggedIn) {
113 | navigate("/profile");
114 | }
115 |
116 | dispatch(RESET());
117 | }, [isLoggedIn, isSuccess, dispatch, navigate]);
118 |
119 | return (
120 |
121 | {isLoading &&
}
122 |
123 |
124 |
125 |
126 |
127 |
Register
128 |
129 |
198 |
199 |
200 | Home
201 | Already have an account?
202 | Login
203 |
204 |
205 |
206 |
207 | );
208 | };
209 |
210 | export default Register;
211 |
--------------------------------------------------------------------------------
/src/pages/auth/Reset.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { MdPassword } from "react-icons/md";
3 | import { useDispatch, useSelector } from "react-redux";
4 |
5 | import { Link, useNavigate, useParams } from "react-router-dom";
6 | import { toast } from "react-toastify";
7 | import Card from "../../components/card/Card";
8 | import Loader from "../../components/loader/Loader";
9 | import PasswordInput from "../../components/passwordInput/PasswordInput";
10 | import { RESET, resetPassword } from "../../redux/features/auth/authSlice";
11 | import styles from "./auth.module.scss";
12 |
13 | const initialState = {
14 | password: "",
15 | password2: "",
16 | };
17 |
18 | const Reset = () => {
19 | const [formData, setFormData] = useState(initialState);
20 | const { password, password2 } = formData;
21 | const { resetToken } = useParams();
22 | console.log(resetToken);
23 |
24 | const { isLoading, isLoggedIn, isSuccess, message } = useSelector(
25 | (state) => state.auth
26 | );
27 | const dispatch = useDispatch();
28 | const navigate = useNavigate();
29 |
30 | const handleInputChange = (e) => {
31 | const { name, value } = e.target;
32 | setFormData({ ...formData, [name]: value });
33 | };
34 |
35 | const reset = async (e) => {
36 | e.preventDefault();
37 |
38 | if (password.length < 6) {
39 | return toast.error("Password must be up to 6 characters");
40 | }
41 | if (password !== password2) {
42 | return toast.error("Passwords do not match");
43 | }
44 |
45 | const userData = {
46 | password,
47 | };
48 |
49 | await dispatch(resetPassword({ userData, resetToken }));
50 | };
51 |
52 | useEffect(() => {
53 | if (isSuccess && message.includes("Reset Successful")) {
54 | navigate("/login");
55 | }
56 |
57 | dispatch(RESET());
58 | }, [dispatch, navigate, message, isSuccess]);
59 |
60 | return (
61 |
62 | {isLoading &&
}
63 |
64 |
65 |
66 |
67 |
68 |
Reset Password
69 |
70 |
96 |
97 |
98 |
99 | );
100 | };
101 |
102 | export default Reset;
103 |
--------------------------------------------------------------------------------
/src/pages/auth/Verify.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { useParams } from "react-router-dom";
4 | import Loader from "../../components/loader/Loader";
5 | import { RESET, verifyUser } from "../../redux/features/auth/authSlice";
6 |
7 | const Verify = () => {
8 | const dispatch = useDispatch();
9 | const { verificationToken } = useParams();
10 |
11 | const { isLoading } = useSelector((state) => state.auth);
12 |
13 | const verifyAccount = async () => {
14 | await dispatch(verifyUser(verificationToken));
15 | await dispatch(RESET());
16 | };
17 |
18 | return (
19 |
20 | {isLoading && }
21 |
22 |
Account Verification
23 |
To verify your account, click the button below...
24 |
25 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default Verify;
34 |
--------------------------------------------------------------------------------
/src/pages/auth/auth.module.scss:
--------------------------------------------------------------------------------
1 | .auth {
2 | min-height: 100vh;
3 | display: flex;
4 | justify-content: center;
5 | align-items: center;
6 |
7 | .img {
8 | animation: slide-down 0.5s ease;
9 | }
10 |
11 | .form {
12 | width: 35rem;
13 | padding: 1.5rem;
14 | animation: slide-up 0.5s ease;
15 | background-color: #fff;
16 | h2 {
17 | color: var(--color-danger);
18 | text-align: center;
19 | }
20 | form {
21 | input[type="text"],
22 | input[type="email"],
23 | input[type="password"] {
24 | display: block;
25 | font-size: 1.6rem;
26 | font-weight: 300;
27 | padding: 1rem;
28 | margin: 1rem auto;
29 | width: 100%;
30 | border: 1px solid #ccc;
31 | border-bottom: 3px solid #ccc;
32 | border-radius: 3px;
33 | outline: none;
34 |
35 | &:focus {
36 | outline: none;
37 | box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.1);
38 | border-bottom: 3px solid #55c57a;
39 | }
40 |
41 | &:focus:invalid {
42 | border-bottom: 3px solid #ff7730;
43 | }
44 | }
45 | .links {
46 | display: flex;
47 | justify-content: space-between;
48 | margin: 5px 0;
49 | }
50 |
51 | p {
52 | text-align: center;
53 | margin: 1rem;
54 | }
55 | }
56 | .register {
57 | display: flex;
58 | justify-content: center;
59 | align-items: center;
60 | margin-top: 1rem;
61 | }
62 | }
63 |
64 | .group {
65 | border: 1px solid var(--light-blue);
66 | padding: 5px;
67 | margin-bottom: 5px;
68 | }
69 |
70 | .indicator {
71 | display: flex;
72 | justify-content: flex-start;
73 | align-items: center;
74 | font-size: 10px;
75 | }
76 |
77 | @keyframes slide-up {
78 | 0% {
79 | transform: translateY(-5rem);
80 | }
81 | 100% {
82 | transform: translateY(0);
83 | }
84 | }
85 | @keyframes slide-down {
86 | 0% {
87 | transform: translateY(5rem);
88 | }
89 | 100% {
90 | transform: translateY(0);
91 | }
92 | }
93 | }
94 |
95 | @media screen and (max-width: 700px) {
96 | .img {
97 | display: none;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/pages/changePassword/ChangePassword.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Card from "../../components/card/Card";
3 | import profileImg from "../../assets/avatarr.png";
4 | import "./ChangePassword.scss";
5 | import PageMenu from "../../components/pageMenu/PageMenu";
6 | import PasswordInput from "../../components/passwordInput/PasswordInput";
7 | import { useNavigate } from "react-router-dom";
8 | import { useDispatch, useSelector } from "react-redux";
9 | import { toast } from "react-toastify";
10 | import useRedirectLoggedOutUser from "../../customHook/useRedirectLoggedOutUser";
11 | import {
12 | changePassword,
13 | logout,
14 | RESET,
15 | } from "../../redux/features/auth/authSlice";
16 | import { Spinner } from "../../components/loader/Loader";
17 | import { sendAutomatedEmail } from "../../redux/features/email/emailSlice";
18 |
19 | const initialState = {
20 | oldPassword: "",
21 | password: "",
22 | password2: "",
23 | };
24 |
25 | const ChangePassword = () => {
26 | useRedirectLoggedOutUser("/login");
27 | const [formData, setFormData] = useState(initialState);
28 | const { oldPassword, password, password2 } = formData;
29 |
30 | const { isLoading, user } = useSelector((state) => state.auth);
31 |
32 | const dispatch = useDispatch();
33 | const navigate = useNavigate();
34 |
35 | const handleInputChange = (e) => {
36 | const { name, value } = e.target;
37 | setFormData({ ...formData, [name]: value });
38 | };
39 |
40 | const updatePassword = async (e) => {
41 | e.preventDefault();
42 |
43 | if (!oldPassword || !password || !password2) {
44 | return toast.error("All fields are required");
45 | }
46 |
47 | if (password !== password2) {
48 | return toast.error("Passwords do not match");
49 | }
50 |
51 | const userData = {
52 | oldPassword,
53 | password,
54 | };
55 |
56 | const emailData = {
57 | subject: "Password Changed - AUTH:Z",
58 | send_to: user.email,
59 | reply_to: "noreply@zino",
60 | template: "changePassword",
61 | url: "/forgot",
62 | };
63 |
64 | await dispatch(changePassword(userData));
65 | await dispatch(sendAutomatedEmail(emailData));
66 | await dispatch(logout());
67 | await dispatch(RESET(userData));
68 | navigate("/login");
69 | };
70 |
71 | return (
72 | <>
73 |
74 |
75 |
76 |
Change Password
77 |
78 |
79 | <>
80 |
119 | >
120 |
121 |
122 |
123 |
124 | >
125 | );
126 | };
127 |
128 | export default ChangePassword;
129 |
--------------------------------------------------------------------------------
/src/pages/changePassword/ChangePassword.scss:
--------------------------------------------------------------------------------
1 | .change-password {
2 | .card {
3 | width: 100%;
4 | max-width: 400px;
5 | padding: 1rem;
6 | border: 1px solid red;
7 | }
8 | .profile-photo {
9 | width: 100%;
10 | // border: 1px solid red;
11 | background-color: var(--light-blue);
12 | padding: 1rem;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | margin-bottom: 1rem;
17 |
18 | & img {
19 | width: 200px;
20 | height: 200px;
21 | border: 1px solid #030b6b;
22 | border-radius: 50%;
23 | }
24 |
25 | & h3 {
26 | color: #fff;
27 | font-size: 2rem;
28 | text-align: center;
29 | }
30 | }
31 | form {
32 | label {
33 | display: block;
34 | font-size: 1.4rem;
35 | font-weight: 500;
36 | }
37 | input[type="text"],
38 | input[type="number"],
39 | input[type="file"],
40 | input[type="email"],
41 | select,
42 | textarea,
43 | input[type="password"] {
44 | display: block;
45 | font-size: 1.6rem;
46 | font-weight: 300;
47 | padding: 1rem;
48 | margin: 1rem auto;
49 | width: 100%;
50 | border: 1px solid #777;
51 | border-radius: 3px;
52 | outline: none;
53 | }
54 |
55 | // textarea {
56 | // width: 100%;
57 | // }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/pages/home/Home.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./Home.scss";
3 | import Footer from "../../components/footer/Footer";
4 | import Header from "../../components/header/Header";
5 | import loginImg from "../../assets/login.svg";
6 | import { Link } from "react-router-dom";
7 |
8 | const Home = () => {
9 | return (
10 |
11 |
12 |
13 |
Ultimate MERN Stack Authentication System
14 |
15 | Learn and Master Authentication and Authorization using MERN Stack.
16 |
17 |
18 | Implement User Regisration, Login, Password Reset, Social Login,
19 | User Permissions, Email Notifications etc.
20 |
21 |
22 |
25 |
28 |
29 |
30 |
31 |
32 |

33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default Home;
40 |
--------------------------------------------------------------------------------
/src/pages/home/Home.scss:
--------------------------------------------------------------------------------
1 | .hero {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | background-color: #eee;
6 | min-height: 80vh;
7 | }
8 |
9 | .hero-text {
10 | width: 50%;
11 | & > * {
12 | color: #333;
13 | margin-bottom: 2rem;
14 | }
15 | & > h2 span {
16 | display: block;
17 | }
18 | & .hero-buttons a {
19 | color: #fff;
20 | }
21 | }
22 | .hero-image {
23 | width: 50%;
24 | text-align: center;
25 | }
26 | .hero-image img {
27 | width: 80%;
28 | }
29 | @media screen and (max-width: 600px) {
30 | .hero {
31 | flex-direction: column;
32 | }
33 |
34 | .hero-text {
35 | display: flex;
36 | justify-content: center;
37 | align-items: center;
38 | flex-direction: column;
39 | width: 100%;
40 | margin: auto;
41 | text-align: center;
42 | }
43 | .hero-image {
44 | width: 100%;
45 | &img {
46 | width: 100%;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/pages/profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useLayoutEffect, useState } from "react";
2 | import Card from "../../components/card/Card";
3 | import profileImg from "../../assets/avatarr.png";
4 | import "./Profile.scss";
5 | import PageMenu from "../../components/pageMenu/PageMenu";
6 | import useRedirectLoggedOutUser from "../../customHook/useRedirectLoggedOutUser";
7 | import { useDispatch, useSelector } from "react-redux";
8 | import {
9 | getUser,
10 | selectUser,
11 | updateUser,
12 | } from "../../redux/features/auth/authSlice";
13 | import Loader from "../../components/loader/Loader";
14 | import { toast } from "react-toastify";
15 | import Notification from "../../components/notification/Notification.js";
16 |
17 | const cloud_name = process.env.REACT_APP_CLOUD_NAME;
18 | const upload_preset = process.env.REACT_APP_UPLOAD_PRESET;
19 |
20 | export const shortenText = (text, n) => {
21 | if (text.length > n) {
22 | const shoretenedText = text.substring(0, n).concat("...");
23 | return shoretenedText;
24 | }
25 | return text;
26 | };
27 |
28 | const Profile = () => {
29 | useRedirectLoggedOutUser("/login");
30 | const dispatch = useDispatch();
31 | const { isLoading, isLoggedIn, isSuccess, message, user } = useSelector(
32 | (state) => state.auth
33 | );
34 | const initialState = {
35 | name: user?.name || "",
36 | email: user?.email || "",
37 | phone: user?.phone || "",
38 | bio: user?.bio || "",
39 | photo: user?.photo || "",
40 | role: user?.role || "",
41 | isVerified: user?.isVerified || false,
42 | };
43 | const [profile, setProfile] = useState(initialState);
44 | const [profileImage, setProfileImage] = useState(null);
45 | const [imagePreview, setImagePreview] = useState(null);
46 |
47 | useEffect(() => {
48 | dispatch(getUser());
49 | }, [dispatch]);
50 |
51 | const handleImageChange = (e) => {
52 | setProfileImage(e.target.files[0]);
53 | setImagePreview(URL.createObjectURL(e.target.files[0]));
54 | };
55 |
56 | const handleInputChange = (e) => {
57 | const { name, value } = e.target;
58 | setProfile({ ...profile, [name]: value });
59 | };
60 |
61 | const saveProfile = async (e) => {
62 | e.preventDefault();
63 | let imageURL;
64 | try {
65 | if (
66 | profileImage !== null &&
67 | (profileImage.type === "image/jpeg" ||
68 | profileImage.type === "image/jpg" ||
69 | profileImage.type === "image/png")
70 | ) {
71 | const image = new FormData();
72 | image.append("file", profileImage);
73 | image.append("cloud_name", cloud_name);
74 | image.append("upload_preset", upload_preset);
75 |
76 | // Save image to Cloudinary
77 | const response = await fetch(
78 | "https://api.cloudinary.com/v1_1/zinotrust/image/upload",
79 | { method: "post", body: image }
80 | );
81 | const imgData = await response.json();
82 | console.log(imgData);
83 | imageURL = imgData.url.toString();
84 | }
85 |
86 | // Save profile to MongoDB
87 | const userData = {
88 | name: profile.name,
89 | phone: profile.phone,
90 | bio: profile.bio,
91 | photo: profileImage ? imageURL : profile.photo,
92 | };
93 |
94 | dispatch(updateUser(userData));
95 | } catch (error) {
96 | toast.error(error.message);
97 | }
98 | };
99 |
100 | useLayoutEffect(() => {
101 | if (user) {
102 | setProfile({
103 | ...profile,
104 | name: user.name,
105 | email: user.email,
106 | phone: user.phone,
107 | photo: user.photo,
108 | bio: user.bio,
109 | role: user.role,
110 | isVerified: user.isVerified,
111 | });
112 | }
113 | }, [user]);
114 |
115 | return (
116 | <>
117 |
118 | {isLoading && }
119 | {!profile.isVerified && }
120 |
121 |
122 |
Profile
123 |
124 |
125 | {!isLoading && user && (
126 | <>
127 |
128 |
129 |

133 |
Role: {profile.role}
134 |
135 |
136 |
188 | >
189 | )}
190 |
191 |
192 |
193 |
194 | >
195 | );
196 | };
197 |
198 | export const UserName = () => {
199 | const user = useSelector(selectUser);
200 |
201 | const username = user?.name || "...";
202 |
203 | return Hi, {shortenText(username, 9)} |
;
204 | };
205 |
206 | export default Profile;
207 |
--------------------------------------------------------------------------------
/src/pages/profile/Profile.scss:
--------------------------------------------------------------------------------
1 | .profile {
2 | .card {
3 | width: 100%;
4 | max-width: 400px;
5 | padding: 1rem;
6 | border: 1px solid #030b6b;
7 | }
8 | .profile-photo {
9 | width: 100%;
10 | // border: 1px solid #fff;
11 | background-color: var(--light-blue);
12 | padding: 1rem;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | margin-bottom: 1rem;
17 |
18 | & img {
19 | width: 200px;
20 | height: 200px;
21 | border: 2px solid #fff;
22 | border-radius: 50%;
23 | }
24 |
25 | & h3 {
26 | color: #fff;
27 | font-size: 2rem;
28 | text-align: center;
29 | }
30 | }
31 | form {
32 | label {
33 | display: block;
34 | font-size: 1.4rem;
35 | font-weight: 500;
36 | }
37 | input[type="text"],
38 | input[type="number"],
39 | input[type="file"],
40 | input[type="email"],
41 | select,
42 | textarea,
43 | input[type="password"] {
44 | display: block;
45 | font-size: 1.6rem;
46 | font-weight: 300;
47 | padding: 1rem;
48 | margin: 1rem auto;
49 | width: 100%;
50 | border: 1px solid #777;
51 | border-radius: 3px;
52 | outline: none;
53 | }
54 |
55 | // textarea {
56 | // width: 100%;
57 | // }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/pages/userList/UserList.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { FaTrashAlt } from "react-icons/fa";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import ChangeRole from "../../components/changeRole/ChangeRole";
5 | import { Spinner } from "../../components/loader/Loader";
6 | import PageMenu from "../../components/pageMenu/PageMenu";
7 | import Search from "../../components/search/Search";
8 | import UserStats from "../../components/userStats/UserStats";
9 | import useRedirectLoggedOutUser from "../../customHook/useRedirectLoggedOutUser";
10 | import { deleteUser, getUsers } from "../../redux/features/auth/authSlice";
11 | import { shortenText } from "../profile/Profile";
12 | import "./UserList.scss";
13 | import { confirmAlert } from "react-confirm-alert";
14 | import "react-confirm-alert/src/react-confirm-alert.css";
15 | import {
16 | FILTER_USERS,
17 | selectUsers,
18 | } from "../../redux/features/auth/filterSlice";
19 | import ReactPaginate from "react-paginate";
20 |
21 | const UserList = () => {
22 | useRedirectLoggedOutUser("/login");
23 | const dispatch = useDispatch();
24 |
25 | const [search, setSearch] = useState("");
26 |
27 | const { users, isLoading, isLoggedIn, isSuccess, message } = useSelector(
28 | (state) => state.auth
29 | );
30 | const filteredUsers = useSelector(selectUsers);
31 |
32 | useEffect(() => {
33 | dispatch(getUsers());
34 | }, [dispatch]);
35 |
36 | const removeUser = async (id) => {
37 | await dispatch(deleteUser(id));
38 | dispatch(getUsers());
39 | };
40 |
41 | const confirmDelete = (id) => {
42 | confirmAlert({
43 | title: "Delete This User",
44 | message: "Are you sure to do delete this user?",
45 | buttons: [
46 | {
47 | label: "Delete",
48 | onClick: () => removeUser(id),
49 | },
50 | {
51 | label: "Cancel",
52 | onClick: () => alert("Click No"),
53 | },
54 | ],
55 | });
56 | };
57 |
58 | useEffect(() => {
59 | dispatch(FILTER_USERS({ users, search }));
60 | }, [dispatch, users, search]);
61 |
62 | // Begin Pagination
63 | const itemsPerPage = 5;
64 | const [itemOffset, setItemOffset] = useState(0);
65 |
66 | const endOffset = itemOffset + itemsPerPage;
67 | const currentItems = filteredUsers.slice(itemOffset, endOffset);
68 | const pageCount = Math.ceil(filteredUsers.length / itemsPerPage);
69 |
70 | // Invoke when user click to request another page.
71 | const handlePageClick = (event) => {
72 | const newOffset = (event.selected * itemsPerPage) % filteredUsers.length;
73 | setItemOffset(newOffset);
74 | };
75 |
76 | // End Pagination
77 |
78 | return (
79 |
80 |
81 |
82 |
83 |
84 |
85 | {isLoading &&
}
86 |
87 |
88 |
89 | All Users
90 |
91 |
92 | setSearch(e.target.value)}
95 | />
96 |
97 |
98 | {/* Table */}
99 | {!isLoading && users.length === 0 ? (
100 |
No user found...
101 | ) : (
102 |
103 |
104 |
105 | s/n |
106 | Name |
107 | Email |
108 | Role |
109 | Change Role |
110 | Action |
111 |
112 |
113 |
114 | {currentItems.map((user, index) => {
115 | const { _id, name, email, role } = user;
116 |
117 | return (
118 |
119 | {index + 1} |
120 | {shortenText(name, 8)} |
121 | {email} |
122 | {role} |
123 |
124 |
125 | |
126 |
127 |
128 | confirmDelete(_id)}
132 | />
133 |
134 | |
135 |
136 | );
137 | })}
138 |
139 |
140 | )}
141 |
142 |
143 |
157 |
158 |
159 |
160 | );
161 | };
162 |
163 | export default UserList;
164 |
--------------------------------------------------------------------------------
/src/pages/userList/UserList.scss:
--------------------------------------------------------------------------------
1 | .user-list {
2 | color: #333;
3 | .table {
4 | padding: 5px;
5 | width: 100%;
6 | overflow-x: auto;
7 |
8 | .search {
9 | width: 100%;
10 | max-width: 300px;
11 | }
12 |
13 | table {
14 | border-collapse: collapse;
15 | width: 100%;
16 | font-size: 1.4rem;
17 |
18 | thead {
19 | border-top: 2px solid var(--light-blue);
20 | border-bottom: 2px solid var(--light-blue);
21 | }
22 |
23 | th {
24 | border: 1px solid #eee;
25 | }
26 |
27 | th,
28 | td {
29 | vertical-align: top;
30 | text-align: left;
31 | padding: 8px;
32 |
33 | &.icons {
34 | display: flex;
35 | justify-content: flex-start;
36 | align-items: center;
37 | > * {
38 | margin-right: 7px;
39 | cursor: pointer;
40 | vertical-align: middle;
41 | align-self: center;
42 | }
43 | }
44 | }
45 |
46 | tr {
47 | border-bottom: 1px solid #ccc;
48 | }
49 |
50 | tr:nth-child(even) {
51 | background-color: #fff;
52 | }
53 | tbody {
54 | tr:hover {
55 | // cursor: pointer;
56 | background-color: rgba(121, 136, 149, 0.3);
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | .sort {
64 | label {
65 | font-size: 1.4rem;
66 | font-weight: 500;
67 | margin: 0 5px;
68 | }
69 | select {
70 | font-size: 1.6rem;
71 | font-weight: 300;
72 | padding: 4px 8px;
73 | margin: 0 5px 0 0;
74 | border: none;
75 | border-bottom: 2px solid #777;
76 | outline: none;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/redux/features/auth/authService.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const BACKEND_URL = process.env.REACT_APP_BACKEND_URL;
4 | export const API_URL = `${BACKEND_URL}/api/users/`;
5 |
6 | // Validate email
7 | export const validateEmail = (email) => {
8 | return email.match(
9 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
10 | );
11 | };
12 |
13 | // Register User
14 | const register = async (userData) => {
15 | const response = await axios.post(API_URL + "register", userData);
16 | return response.data;
17 | };
18 |
19 | // Login User
20 | const login = async (userData) => {
21 | const response = await axios.post(API_URL + "login", userData);
22 | return response.data;
23 | };
24 |
25 | // Logout User
26 | const logout = async () => {
27 | const response = await axios.get(API_URL + "logout");
28 | return response.data.message;
29 | };
30 |
31 | // Get Login Status
32 | const getLoginStatus = async () => {
33 | const response = await axios.get(API_URL + "loginStatus");
34 | return response.data;
35 | };
36 |
37 | // Get user profile
38 | const getUser = async () => {
39 | const response = await axios.get(API_URL + "getUser");
40 | return response.data;
41 | };
42 |
43 | // Update profile
44 | const updateUser = async (userData) => {
45 | const response = await axios.patch(API_URL + "updateUser", userData);
46 | return response.data;
47 | };
48 |
49 | // Send Verification Email
50 | const sendVerificationEmail = async () => {
51 | const response = await axios.post(API_URL + "sendVerificationEmail");
52 | return response.data.message;
53 | };
54 |
55 | // Verify User
56 | const verifyUser = async (verificationToken) => {
57 | const response = await axios.patch(
58 | `${API_URL}verifyUser/${verificationToken}`
59 | );
60 |
61 | return response.data.message;
62 | };
63 |
64 | // Change Password
65 | const changePassword = async (userData) => {
66 | const response = await axios.patch(API_URL + "changePassword", userData);
67 |
68 | return response.data.message;
69 | };
70 |
71 | // Reset Password
72 | const resetPassword = async (userData, resetToken) => {
73 | const response = await axios.patch(
74 | `${API_URL}resetPassword/${resetToken}`,
75 | userData
76 | );
77 |
78 | return response.data.message;
79 | };
80 |
81 | // fORGOT Password
82 | const forgotPassword = async (userData) => {
83 | const response = await axios.post(API_URL + "forgotPassword", userData);
84 |
85 | return response.data.message;
86 | };
87 |
88 | // Get Users
89 | const getUsers = async () => {
90 | const response = await axios.get(API_URL + "getUsers");
91 |
92 | return response.data;
93 | };
94 | // Delete User
95 | const deleteUser = async (id) => {
96 | const response = await axios.delete(API_URL + id);
97 |
98 | return response.data.message;
99 | };
100 |
101 | // Upgrade User
102 | const upgradeUser = async (userData) => {
103 | const response = await axios.post(API_URL + "upgradeUser", userData);
104 |
105 | return response.data.message;
106 | };
107 |
108 | // Send Login Code
109 | const sendLoginCode = async (email) => {
110 | const response = await axios.post(API_URL + `sendLoginCode/${email}`);
111 |
112 | return response.data.message;
113 | };
114 | // Login With Code
115 | const loginWithCode = async (code, email) => {
116 | const response = await axios.post(API_URL + `loginWithCode/${email}`, code);
117 |
118 | return response.data;
119 | };
120 | // Login With Googlr
121 | const loginWithGoogle = async (userToken) => {
122 | const response = await axios.post(API_URL + "google/callback", userToken);
123 |
124 | return response.data;
125 | };
126 |
127 | const authService = {
128 | register,
129 | login,
130 | logout,
131 | getLoginStatus,
132 | getUser,
133 | updateUser,
134 | sendVerificationEmail,
135 | verifyUser,
136 | changePassword,
137 | forgotPassword,
138 | resetPassword,
139 | getUsers,
140 | deleteUser,
141 | upgradeUser,
142 | sendLoginCode,
143 | loginWithCode,
144 | loginWithGoogle,
145 | };
146 |
147 | export default authService;
148 |
--------------------------------------------------------------------------------
/src/redux/features/auth/authSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import { toast } from "react-toastify";
3 | import authService from "./authService";
4 |
5 | const initialState = {
6 | isLoggedIn: false,
7 | user: null,
8 | users: [],
9 | twoFactor: false,
10 | isError: false,
11 | isSuccess: false,
12 | isLoading: false,
13 | message: "",
14 | verifiedUsers: 0,
15 | suspendedUsers: 0,
16 | };
17 |
18 | // Register User
19 | export const register = createAsyncThunk(
20 | "auth/register",
21 | async (userData, thunkAPI) => {
22 | try {
23 | return await authService.register(userData);
24 | } catch (error) {
25 | const message =
26 | (error.response &&
27 | error.response.data &&
28 | error.response.data.message) ||
29 | error.message ||
30 | error.toString();
31 | return thunkAPI.rejectWithValue(message);
32 | }
33 | }
34 | );
35 |
36 | // Login User
37 | export const login = createAsyncThunk(
38 | "auth/login",
39 | async (userData, thunkAPI) => {
40 | try {
41 | return await authService.login(userData);
42 | } catch (error) {
43 | const message =
44 | (error.response &&
45 | error.response.data &&
46 | error.response.data.message) ||
47 | error.message ||
48 | error.toString();
49 | return thunkAPI.rejectWithValue(message);
50 | }
51 | }
52 | );
53 |
54 | // Logout User
55 | export const logout = createAsyncThunk("auth/logout", async (_, thunkAPI) => {
56 | try {
57 | return await authService.logout();
58 | } catch (error) {
59 | const message =
60 | (error.response && error.response.data && error.response.data.message) ||
61 | error.message ||
62 | error.toString();
63 | return thunkAPI.rejectWithValue(message);
64 | }
65 | });
66 |
67 | // Get Login Status
68 | export const getLoginStatus = createAsyncThunk(
69 | "auth/getLoginStatus",
70 | async (_, thunkAPI) => {
71 | try {
72 | return await authService.getLoginStatus();
73 | } catch (error) {
74 | const message =
75 | (error.response &&
76 | error.response.data &&
77 | error.response.data.message) ||
78 | error.message ||
79 | error.toString();
80 | return thunkAPI.rejectWithValue(message);
81 | }
82 | }
83 | );
84 |
85 | // Get User
86 | export const getUser = createAsyncThunk("auth/getUser", async (_, thunkAPI) => {
87 | try {
88 | return await authService.getUser();
89 | } catch (error) {
90 | const message =
91 | (error.response && error.response.data && error.response.data.message) ||
92 | error.message ||
93 | error.toString();
94 | return thunkAPI.rejectWithValue(message);
95 | }
96 | });
97 |
98 | // Update User
99 | export const updateUser = createAsyncThunk(
100 | "auth/updateUser",
101 | async (userData, thunkAPI) => {
102 | try {
103 | return await authService.updateUser(userData);
104 | } catch (error) {
105 | const message =
106 | (error.response &&
107 | error.response.data &&
108 | error.response.data.message) ||
109 | error.message ||
110 | error.toString();
111 | return thunkAPI.rejectWithValue(message);
112 | }
113 | }
114 | );
115 |
116 | // send Verification Email
117 | export const sendVerificationEmail = createAsyncThunk(
118 | "auth/sendVerificationEmail",
119 | async (_, thunkAPI) => {
120 | try {
121 | return await authService.sendVerificationEmail();
122 | } catch (error) {
123 | const message =
124 | (error.response &&
125 | error.response.data &&
126 | error.response.data.message) ||
127 | error.message ||
128 | error.toString();
129 | return thunkAPI.rejectWithValue(message);
130 | }
131 | }
132 | );
133 |
134 | // verify User
135 | export const verifyUser = createAsyncThunk(
136 | "auth/verifyUser",
137 | async (verificationToken, thunkAPI) => {
138 | try {
139 | return await authService.verifyUser(verificationToken);
140 | } catch (error) {
141 | const message =
142 | (error.response &&
143 | error.response.data &&
144 | error.response.data.message) ||
145 | error.message ||
146 | error.toString();
147 | return thunkAPI.rejectWithValue(message);
148 | }
149 | }
150 | );
151 |
152 | // change Password
153 | export const changePassword = createAsyncThunk(
154 | "auth/changePassword",
155 | async (userData, thunkAPI) => {
156 | try {
157 | return await authService.changePassword(userData);
158 | } catch (error) {
159 | const message =
160 | (error.response &&
161 | error.response.data &&
162 | error.response.data.message) ||
163 | error.message ||
164 | error.toString();
165 | return thunkAPI.rejectWithValue(message);
166 | }
167 | }
168 | );
169 |
170 | // forgot Password
171 | export const forgotPassword = createAsyncThunk(
172 | "auth/forgotPassword",
173 | async (userData, thunkAPI) => {
174 | try {
175 | return await authService.forgotPassword(userData);
176 | } catch (error) {
177 | const message =
178 | (error.response &&
179 | error.response.data &&
180 | error.response.data.message) ||
181 | error.message ||
182 | error.toString();
183 | return thunkAPI.rejectWithValue(message);
184 | }
185 | }
186 | );
187 |
188 | // resetPassword
189 | export const resetPassword = createAsyncThunk(
190 | "auth/resetPassword",
191 | async ({ userData, resetToken }, thunkAPI) => {
192 | try {
193 | return await authService.resetPassword(userData, resetToken);
194 | } catch (error) {
195 | const message =
196 | (error.response &&
197 | error.response.data &&
198 | error.response.data.message) ||
199 | error.message ||
200 | error.toString();
201 | return thunkAPI.rejectWithValue(message);
202 | }
203 | }
204 | );
205 | // getUsers
206 | export const getUsers = createAsyncThunk(
207 | "auth/getUsers",
208 | async (_, thunkAPI) => {
209 | try {
210 | return await authService.getUsers();
211 | } catch (error) {
212 | const message =
213 | (error.response &&
214 | error.response.data &&
215 | error.response.data.message) ||
216 | error.message ||
217 | error.toString();
218 | return thunkAPI.rejectWithValue(message);
219 | }
220 | }
221 | );
222 | // deleteUser
223 | export const deleteUser = createAsyncThunk(
224 | "auth/deleteUser",
225 | async (id, thunkAPI) => {
226 | try {
227 | return await authService.deleteUser(id);
228 | } catch (error) {
229 | const message =
230 | (error.response &&
231 | error.response.data &&
232 | error.response.data.message) ||
233 | error.message ||
234 | error.toString();
235 | return thunkAPI.rejectWithValue(message);
236 | }
237 | }
238 | );
239 | // upgradeUser
240 | export const upgradeUser = createAsyncThunk(
241 | "auth/upgradeUser",
242 | async (userData, thunkAPI) => {
243 | try {
244 | return await authService.upgradeUser(userData);
245 | } catch (error) {
246 | const message =
247 | (error.response &&
248 | error.response.data &&
249 | error.response.data.message) ||
250 | error.message ||
251 | error.toString();
252 | return thunkAPI.rejectWithValue(message);
253 | }
254 | }
255 | );
256 |
257 | // sendLoginCode
258 | export const sendLoginCode = createAsyncThunk(
259 | "auth/sendLoginCode",
260 | async (email, thunkAPI) => {
261 | try {
262 | return await authService.sendLoginCode(email);
263 | } catch (error) {
264 | const message =
265 | (error.response &&
266 | error.response.data &&
267 | error.response.data.message) ||
268 | error.message ||
269 | error.toString();
270 | return thunkAPI.rejectWithValue(message);
271 | }
272 | }
273 | );
274 |
275 | // loginWithCode
276 | export const loginWithCode = createAsyncThunk(
277 | "auth/loginWithCode",
278 | async ({ code, email }, thunkAPI) => {
279 | try {
280 | return await authService.loginWithCode(code, email);
281 | } catch (error) {
282 | const message =
283 | (error.response &&
284 | error.response.data &&
285 | error.response.data.message) ||
286 | error.message ||
287 | error.toString();
288 | return thunkAPI.rejectWithValue(message);
289 | }
290 | }
291 | );
292 | // loginWithGoogle
293 | export const loginWithGoogle = createAsyncThunk(
294 | "auth/loginWithGoogle",
295 | async (userToken, thunkAPI) => {
296 | try {
297 | return await authService.loginWithGoogle(userToken);
298 | } catch (error) {
299 | const message =
300 | (error.response &&
301 | error.response.data &&
302 | error.response.data.message) ||
303 | error.message ||
304 | error.toString();
305 | return thunkAPI.rejectWithValue(message);
306 | }
307 | }
308 | );
309 |
310 | const authSlice = createSlice({
311 | name: "auth",
312 | initialState,
313 | reducers: {
314 | RESET(state) {
315 | state.twoFactor = false;
316 | state.isError = false;
317 | state.isSuccess = false;
318 | state.isLoading = false;
319 | state.message = "";
320 | },
321 | CALC_VERIFIED_USER(state, action) {
322 | const array = [];
323 | state.users.map((user) => {
324 | const { isVerified } = user;
325 | return array.push(isVerified);
326 | });
327 | let count = 0;
328 | array.forEach((item) => {
329 | if (item === true) {
330 | count += 1;
331 | }
332 | });
333 | state.verifiedUsers = count;
334 | },
335 | CALC_SUSPENDED_USER(state, action) {
336 | const array = [];
337 | state.users.map((user) => {
338 | const { role } = user;
339 | return array.push(role);
340 | });
341 | let count = 0;
342 | array.forEach((item) => {
343 | if (item === "suspended") {
344 | count += 1;
345 | }
346 | });
347 | state.suspendedUsers = count;
348 | },
349 | },
350 | extraReducers: (builder) => {
351 | builder
352 | // Register User
353 | .addCase(register.pending, (state) => {
354 | state.isLoading = true;
355 | })
356 | .addCase(register.fulfilled, (state, action) => {
357 | state.isLoading = false;
358 | state.isSuccess = true;
359 | state.isLoggedIn = true;
360 | state.user = action.payload;
361 | toast.success("Registration Successful");
362 | console.log(action.payload);
363 | })
364 | .addCase(register.rejected, (state, action) => {
365 | state.isLoading = false;
366 | state.isError = true;
367 | state.message = action.payload;
368 | state.user = null;
369 | toast.error(action.payload);
370 | })
371 | // Login User
372 | .addCase(login.pending, (state) => {
373 | state.isLoading = true;
374 | })
375 | .addCase(login.fulfilled, (state, action) => {
376 | state.isLoading = false;
377 | state.isSuccess = true;
378 | state.isLoggedIn = true;
379 | state.user = action.payload;
380 | toast.success("Login Successful");
381 | console.log(action.payload);
382 | })
383 | .addCase(login.rejected, (state, action) => {
384 | state.isLoading = false;
385 | state.isError = true;
386 | state.message = action.payload;
387 | state.user = null;
388 | toast.error(action.payload);
389 | if (action.payload.includes("New browser")) {
390 | state.twoFactor = true;
391 | }
392 | })
393 |
394 | // Logout User
395 | .addCase(logout.pending, (state) => {
396 | state.isLoading = true;
397 | })
398 | .addCase(logout.fulfilled, (state, action) => {
399 | state.isLoading = false;
400 | state.isSuccess = true;
401 | state.isLoggedIn = false;
402 | state.user = null;
403 | toast.success(action.payload);
404 | })
405 | .addCase(logout.rejected, (state, action) => {
406 | state.isLoading = false;
407 | state.isError = true;
408 | state.message = action.payload;
409 | toast.error(action.payload);
410 | })
411 |
412 | // Get Login Status
413 | .addCase(getLoginStatus.pending, (state) => {
414 | state.isLoading = true;
415 | })
416 | .addCase(getLoginStatus.fulfilled, (state, action) => {
417 | state.isLoading = false;
418 | state.isSuccess = true;
419 | state.isLoggedIn = action.payload;
420 | console.log(action.payload);
421 | })
422 | .addCase(getLoginStatus.rejected, (state, action) => {
423 | state.isLoading = false;
424 | state.isError = true;
425 | state.message = action.payload;
426 | console.log(action.payload);
427 | })
428 |
429 | // Get User
430 | .addCase(getUser.pending, (state) => {
431 | state.isLoading = true;
432 | })
433 | .addCase(getUser.fulfilled, (state, action) => {
434 | state.isLoading = false;
435 | state.isSuccess = true;
436 | state.isLoggedIn = true;
437 | state.user = action.payload;
438 | })
439 | .addCase(getUser.rejected, (state, action) => {
440 | state.isLoading = false;
441 | state.isError = true;
442 | state.message = action.payload;
443 | toast.error(action.payload);
444 | })
445 |
446 | // Update user
447 | .addCase(updateUser.pending, (state) => {
448 | state.isLoading = true;
449 | })
450 | .addCase(updateUser.fulfilled, (state, action) => {
451 | state.isLoading = false;
452 | state.isSuccess = true;
453 | state.isLoggedIn = true;
454 | state.user = action.payload;
455 | toast.success("User Updated");
456 | })
457 | .addCase(updateUser.rejected, (state, action) => {
458 | state.isLoading = false;
459 | state.isError = true;
460 | state.message = action.payload;
461 | toast.error(action.payload);
462 | })
463 |
464 | // send Verification Email
465 | .addCase(sendVerificationEmail.pending, (state) => {
466 | state.isLoading = true;
467 | })
468 | .addCase(sendVerificationEmail.fulfilled, (state, action) => {
469 | state.isLoading = false;
470 | state.isSuccess = true;
471 | state.message = action.payload;
472 | toast.success(action.payload);
473 | })
474 | .addCase(sendVerificationEmail.rejected, (state, action) => {
475 | state.isLoading = false;
476 | state.isError = true;
477 | state.message = action.payload;
478 | toast.error(action.payload);
479 | })
480 |
481 | // verify User
482 | .addCase(verifyUser.pending, (state) => {
483 | state.isLoading = true;
484 | })
485 | .addCase(verifyUser.fulfilled, (state, action) => {
486 | state.isLoading = false;
487 | state.isSuccess = true;
488 | state.message = action.payload;
489 | toast.success(action.payload);
490 | })
491 | .addCase(verifyUser.rejected, (state, action) => {
492 | state.isLoading = false;
493 | state.isError = true;
494 | state.message = action.payload;
495 | toast.error(action.payload);
496 | })
497 |
498 | // change Password
499 | .addCase(changePassword.pending, (state) => {
500 | state.isLoading = true;
501 | })
502 | .addCase(changePassword.fulfilled, (state, action) => {
503 | state.isLoading = false;
504 | state.isSuccess = true;
505 | state.message = action.payload;
506 | toast.success(action.payload);
507 | })
508 | .addCase(changePassword.rejected, (state, action) => {
509 | state.isLoading = false;
510 | state.isError = true;
511 | state.message = action.payload;
512 | toast.error(action.payload);
513 | })
514 |
515 | // forgotPassword
516 | .addCase(forgotPassword.pending, (state) => {
517 | state.isLoading = true;
518 | })
519 | .addCase(forgotPassword.fulfilled, (state, action) => {
520 | state.isLoading = false;
521 | state.isSuccess = true;
522 | state.message = action.payload;
523 | toast.success(action.payload);
524 | })
525 | .addCase(forgotPassword.rejected, (state, action) => {
526 | state.isLoading = false;
527 | state.isError = true;
528 | state.message = action.payload;
529 | toast.error(action.payload);
530 | })
531 |
532 | // resetPassword
533 | .addCase(resetPassword.pending, (state) => {
534 | state.isLoading = true;
535 | })
536 | .addCase(resetPassword.fulfilled, (state, action) => {
537 | state.isLoading = false;
538 | state.isSuccess = true;
539 | state.message = action.payload;
540 | toast.success(action.payload);
541 | })
542 | .addCase(resetPassword.rejected, (state, action) => {
543 | state.isLoading = false;
544 | state.isError = true;
545 | state.message = action.payload;
546 | toast.error(action.payload);
547 | })
548 |
549 | // getUsers
550 | .addCase(getUsers.pending, (state) => {
551 | state.isLoading = true;
552 | })
553 | .addCase(getUsers.fulfilled, (state, action) => {
554 | state.isLoading = false;
555 | state.isSuccess = true;
556 | state.users = action.payload;
557 | })
558 | .addCase(getUsers.rejected, (state, action) => {
559 | state.isLoading = false;
560 | state.isError = true;
561 | state.message = action.payload;
562 | toast.error(action.payload);
563 | })
564 |
565 | // deleteUser
566 | .addCase(deleteUser.pending, (state) => {
567 | state.isLoading = true;
568 | })
569 | .addCase(deleteUser.fulfilled, (state, action) => {
570 | state.isLoading = false;
571 | state.isSuccess = true;
572 | state.message = action.payload;
573 | toast.success(action.payload);
574 | })
575 | .addCase(deleteUser.rejected, (state, action) => {
576 | state.isLoading = false;
577 | state.isError = true;
578 | state.message = action.payload;
579 | toast.error(action.payload);
580 | })
581 |
582 | // upgradeUser
583 | .addCase(upgradeUser.pending, (state) => {
584 | state.isLoading = true;
585 | })
586 | .addCase(upgradeUser.fulfilled, (state, action) => {
587 | state.isLoading = false;
588 | state.isSuccess = true;
589 | state.message = action.payload;
590 | toast.success(action.payload);
591 | })
592 | .addCase(upgradeUser.rejected, (state, action) => {
593 | state.isLoading = false;
594 | state.isError = true;
595 | state.message = action.payload;
596 | toast.error(action.payload);
597 | })
598 |
599 | // send Login Code
600 | .addCase(sendLoginCode.pending, (state) => {
601 | state.isLoading = true;
602 | })
603 | .addCase(sendLoginCode.fulfilled, (state, action) => {
604 | state.isLoading = false;
605 | state.isSuccess = true;
606 | state.message = action.payload;
607 | toast.success(action.payload);
608 | })
609 | .addCase(sendLoginCode.rejected, (state, action) => {
610 | state.isLoading = false;
611 | state.isError = true;
612 | state.message = action.payload;
613 | toast.error(action.payload);
614 | })
615 |
616 | // loginWithCode
617 | .addCase(loginWithCode.pending, (state) => {
618 | state.isLoading = true;
619 | })
620 | .addCase(loginWithCode.fulfilled, (state, action) => {
621 | state.isLoading = false;
622 | state.isSuccess = true;
623 | state.isLoggedIn = true;
624 | state.twoFactor = false;
625 | state.user = action.payload;
626 | toast.success(action.payload);
627 | })
628 | .addCase(loginWithCode.rejected, (state, action) => {
629 | state.isLoading = false;
630 | state.isError = true;
631 | state.message = action.payload;
632 | state.user = null;
633 | toast.error(action.payload);
634 | })
635 |
636 | // loginWithGoogle
637 | .addCase(loginWithGoogle.pending, (state) => {
638 | state.isLoading = true;
639 | })
640 | .addCase(loginWithGoogle.fulfilled, (state, action) => {
641 | state.isLoading = false;
642 | state.isSuccess = true;
643 | state.isLoggedIn = true;
644 | state.user = action.payload;
645 | toast.success("Login Successful");
646 | })
647 | .addCase(loginWithGoogle.rejected, (state, action) => {
648 | state.isLoading = false;
649 | state.isError = true;
650 | state.message = action.payload;
651 | state.user = null;
652 | toast.error(action.payload);
653 | });
654 | },
655 | });
656 |
657 | export const { RESET, CALC_VERIFIED_USER, CALC_SUSPENDED_USER } =
658 | authSlice.actions;
659 |
660 | export const selectIsLoggedIn = (state) => state.auth.isLoggedIn;
661 | export const selectUser = (state) => state.auth.user;
662 |
663 | export default authSlice.reducer;
664 |
--------------------------------------------------------------------------------
/src/redux/features/auth/filterSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | filteredUsers: [],
5 | };
6 |
7 | const filterSlice = createSlice({
8 | name: "filter",
9 | initialState,
10 | reducers: {
11 | FILTER_USERS(state, action) {
12 | const { users, search } = action.payload;
13 | const tempUsers = users.filter(
14 | (user) =>
15 | user.name.toLowerCase().includes(search.toLowerCase()) ||
16 | user.email.toLowerCase().includes(search.toLowerCase())
17 | );
18 | state.filteredUsers = tempUsers;
19 | },
20 | },
21 | });
22 |
23 | export const { FILTER_USERS } = filterSlice.actions;
24 |
25 | export const selectUsers = (state) => state.filter.filteredUsers;
26 |
27 | export default filterSlice.reducer;
28 |
--------------------------------------------------------------------------------
/src/redux/features/email/emailService.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import { API_URL } from "../auth/authService";
3 |
4 | // sEND Automated Email
5 | const sendAutomatedEmail = async (emailData) => {
6 | const response = await axios.post(API_URL + "sendAutomatedEmail", emailData);
7 | return response.data.message;
8 | };
9 |
10 | const emailService = {
11 | sendAutomatedEmail,
12 | };
13 |
14 | export default emailService;
15 |
--------------------------------------------------------------------------------
/src/redux/features/email/emailSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
2 | import { toast } from "react-toastify";
3 | import emailService from "./emailService";
4 |
5 | const initialState = {
6 | sendingEmail: false,
7 | emailSent: false,
8 | msg: "",
9 | };
10 |
11 | // Send Automated Email
12 | export const sendAutomatedEmail = createAsyncThunk(
13 | "email/sendAutomatedEmail",
14 | async (userData, thunkAPI) => {
15 | try {
16 | return await emailService.sendAutomatedEmail(userData);
17 | } catch (error) {
18 | const message =
19 | (error.response &&
20 | error.response.data &&
21 | error.response.data.message) ||
22 | error.message ||
23 | error.toString();
24 | return thunkAPI.rejectWithValue(message);
25 | }
26 | }
27 | );
28 |
29 | const emailSlice = createSlice({
30 | name: "email",
31 | initialState,
32 | reducers: {
33 | EMAIL_RESET(state) {
34 | state.sendingEmail = false;
35 | state.emailSent = false;
36 | state.msg = "";
37 | },
38 | },
39 | extraReducers: (builder) => {
40 | builder
41 |
42 | // sendAutomated email
43 | .addCase(sendAutomatedEmail.pending, (state) => {
44 | state.sendingEmail = true;
45 | })
46 | .addCase(sendAutomatedEmail.fulfilled, (state, action) => {
47 | state.sendingEmail = true;
48 | state.emailSent = true;
49 | state.msg = action.payload;
50 | toast.success(action.payload);
51 | })
52 | .addCase(sendAutomatedEmail.rejected, (state, action) => {
53 | state.sendingEmail = false;
54 | state.emailSent = false;
55 | state.msg = action.payload;
56 | toast.success(action.payload);
57 | });
58 | },
59 | });
60 |
61 | export const { EMAIL_RESET } = emailSlice.actions;
62 |
63 | export default emailSlice.reducer;
64 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import authReducer from "../redux/features/auth/authSlice";
3 | import emailReducer from "../redux/features/email/emailSlice";
4 | import filterReducer from "../redux/features/auth/filterSlice";
5 |
6 | export const store = configureStore({
7 | reducer: {
8 | auth: authReducer,
9 | email: emailReducer,
10 | filter: filterReducer,
11 | },
12 | });
13 |
--------------------------------------------------------------------------------