diff --git a/package-lock.json b/package-lock.json index 634d057..39ebb5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,8 @@ "react-dom": "^18.2.0", "react-redux": "^9.1.1", "react-router": "^6.22.3", - "react-router-dom": "^6.22.3" + "react-router-dom": "^6.22.3", + "react-toastify": "^10.0.5" }, "devDependencies": { "@parcel/transformer-sass": "^2.12.0", @@ -2823,6 +2824,14 @@ "node": ">=0.8" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5839,6 +5848,18 @@ "react-dom": ">=16.8" } }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", diff --git a/package.json b/package.json index 67753a0..e6ada44 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "react-dom": "^18.2.0", "react-redux": "^9.1.1", "react-router": "^6.22.3", - "react-router-dom": "^6.22.3" + "react-router-dom": "^6.22.3", + "react-toastify": "^10.0.5" }, "devDependencies": { "@parcel/transformer-sass": "^2.12.0", diff --git a/src/components/Icons/FavoriteIcon/FavoriteIcon.jsx b/src/components/Icons/FavoriteIcon/FavoriteIcon.jsx index ea9fe3d..8d721f7 100644 --- a/src/components/Icons/FavoriteIcon/FavoriteIcon.jsx +++ b/src/components/Icons/FavoriteIcon/FavoriteIcon.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Button } from 'react-bootstrap'; import { useSelector, useDispatch } from 'react-redux'; import { setUser } from '../../../state/user/userSlice'; +import { toast } from 'react-toastify'; import { moviePropTypes } from '../../../propTypes'; @@ -25,13 +26,14 @@ export const FavoriteIcon = ({ movie, className = '' }) => { if (data.success) { const updatedUser = data.data; dispatch(setUser(updatedUser)); + toast.success(isFavorite ? 'Movie removed' : 'Movie added'); } else { - alert(`Operation failed: ${data.error.message}`); + toast.error(`${data.error.message}`); } }) .catch((error) => { + toast.error('Something went wrong!'); console.log(error); - alert('Something went wrong'); }); }; diff --git a/src/components/Icons/ToWatchIcon/ToWatchIcon.jsx b/src/components/Icons/ToWatchIcon/ToWatchIcon.jsx index e42f3ce..9b0f219 100644 --- a/src/components/Icons/ToWatchIcon/ToWatchIcon.jsx +++ b/src/components/Icons/ToWatchIcon/ToWatchIcon.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { Button } from 'react-bootstrap'; import { useSelector, useDispatch } from 'react-redux'; import { setUser } from '../../../state/user/userSlice'; +import { toast } from 'react-toastify'; import { moviePropTypes } from '../../../propTypes'; @@ -25,13 +26,14 @@ export const ToWatchIcon = ({ movie, className = '' }) => { if (data.success) { const updatedUser = data.data; dispatch(setUser(updatedUser)); + toast.success(isAdded ? 'Movie removed' : 'Movie added'); } else { - alert(`Operation failed: ${data.error.message}`); + toast.error(`${data.error.message}`); } }) .catch((error) => { + toast.error('Something went wrong!'); console.log(error); - alert('Something went wrong'); }); }; diff --git a/src/index.jsx b/src/index.jsx index ee1484c..3edb6e8 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -4,6 +4,7 @@ import { Provider } from 'react-redux'; import { store } from './state/store'; import { HomePage } from './pages'; +import 'react-toastify/dist/ReactToastify.min.css'; import './index.scss'; const App = () => { diff --git a/src/layouts/AppLayout/AppLayout.jsx b/src/layouts/AppLayout/AppLayout.jsx index 2a903d2..c7783ad 100644 --- a/src/layouts/AppLayout/AppLayout.jsx +++ b/src/layouts/AppLayout/AppLayout.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { Outlet } from 'react-router-dom'; import { Container, Row, Col } from 'react-bootstrap'; +import { ToastContainer } from 'react-toastify'; import { NavigationBar, FooterBar } from '../../layouts'; export const AppLayout = () => { @@ -15,6 +16,7 @@ export const AppLayout = () => { + diff --git a/src/pages/LoginPage/LoginPage.jsx b/src/pages/LoginPage/LoginPage.jsx index 50cc647..38363f3 100644 --- a/src/pages/LoginPage/LoginPage.jsx +++ b/src/pages/LoginPage/LoginPage.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Button, Form, Spinner } from 'react-bootstrap'; import { Navigate, Link } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; +import { toast } from 'react-toastify'; import { setUser, setToken } from '../../state/user/userSlice'; export const LoginPage = () => { @@ -42,15 +43,16 @@ export const LoginPage = () => { const { user, token } = data.data; dispatch(setUser(user)); dispatch(setToken(token)); + toast.success(`Welcome back, ${user.Name}`); } else { - alert(`Login failed: ${data.error.message}`); + toast.error(`Login failed: ${data.error.message}`); } setLoading(false); }) .catch((error) => { setLoading(false); - alert('Something went wrong'); - console.log(error); + toast.error('Something went wrong!'); + console.error(error); }); }; diff --git a/src/pages/ProfilePage/ProfilePage.jsx b/src/pages/ProfilePage/ProfilePage.jsx index 03c32a7..8492dcd 100644 --- a/src/pages/ProfilePage/ProfilePage.jsx +++ b/src/pages/ProfilePage/ProfilePage.jsx @@ -3,6 +3,7 @@ import { Row, Col, Button, Form, Spinner } from 'react-bootstrap'; import { MoviesSlider } from '../../components/MoviesSlider'; import { useSelector, useDispatch } from 'react-redux'; import { setUser, onLoggedOut } from '../../state/user/userSlice'; +import { toast } from 'react-toastify'; export const ProfilePage = () => { // TODO: Fix the bug with timezone @@ -26,6 +27,7 @@ export const ProfilePage = () => { const [name, setName] = useState(user.Name); const [password, setPassword] = useState(''); const [birthday, setBirthday] = useState(formattedDate(user.Birthday)); + const [errors, setErrors] = useState([]); const movies = useSelector((state) => state.movies.list); @@ -64,15 +66,21 @@ export const ProfilePage = () => { .then((data) => { if (data.success) { dispatch(setUser({ ...user, ...data.data })); - alert('Update successful'); + setErrors([]); + toast.success('Update successful'); } else { - alert(`Update failed: ${data.error.message}`); + const errors = data.error.message; + if (Array.isArray(errors)) { + setErrors(errors.reduce((acc, cur) => ({ ...acc, [cur.path]: cur.msg }), {})); + } else { + toast.error(errors); + } } setLoading(false); }) .catch((error) => { setLoading(false); - alert('Something went wrong'); + toast.error('Something went wrong!'); console.log(error); }); }; @@ -124,9 +132,11 @@ export const ProfilePage = () => { placeholder="Name" defaultValue={user.Name} onChange={(e) => setName(e.target.value)} + isInvalid={!!errors.Name} required minLength={5} /> + {errors.Name} diff --git a/src/pages/SignUpPage/SignUpPage.jsx b/src/pages/SignUpPage/SignUpPage.jsx index 130d18b..ed9bbc3 100644 --- a/src/pages/SignUpPage/SignUpPage.jsx +++ b/src/pages/SignUpPage/SignUpPage.jsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Button, Form, Spinner } from 'react-bootstrap'; import { Navigate, Link, useNavigate } from 'react-router-dom'; import { useSelector } from 'react-redux'; +import { toast } from 'react-toastify'; export const SignUpPage = () => { const navigate = useNavigate(); @@ -12,10 +13,9 @@ export const SignUpPage = () => { const [email, setEmail] = useState(''); const [birthday, setBirthday] = useState(''); const [loading, setLoading] = useState(false); + const [errors, setErrors] = useState([]); const handleSubmit = (event) => { - // this prevents the default behavior of the form which is to - // reload the entire page event.preventDefault(); const data = { @@ -41,16 +41,21 @@ export const SignUpPage = () => { // Please review the response format of the API here // https://cf-2-movie-api.onrender.com/docs/#/User/post_users if (data.success) { - alert('Signup successful'); + toast.success('Signup successful'); navigate('/', { replace: true }); } else { - alert(`Signup failed: ${data.error.message}`); + const errors = data.error.message; + if (Array.isArray(errors)) { + setErrors(errors.reduce((acc, cur) => ({ ...acc, [cur.path]: cur.msg }), {})); + } else { + setErrors({ Email: errors }); + } } setLoading(false); }) .catch((error) => { setLoading(false); - alert('Something went wrong'); + toast.error('Something went wrong!'); console.log(error); }); }; @@ -75,8 +80,10 @@ export const SignUpPage = () => { value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" - required + isInvalid={!!errors.Email} + // required /> + {errors.Email} @@ -85,8 +92,10 @@ export const SignUpPage = () => { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" + isInvalid={!!errors.Password} required /> + {errors.Password} @@ -95,9 +104,11 @@ export const SignUpPage = () => { value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" + isInvalid={!!errors.Name} required minLength="5" /> + {errors.Name}