diff --git a/package-lock.json b/package-lock.json index 99943fb..e012d1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,9 +16,11 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.2", + "date-fns": "^2.30.0", "formik": "^2.4.5", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-datepicker": "^4.24.0", "react-dom": "^18.2.0", "react-icons": "^4.12.0", "react-redux": "^9.0.0", @@ -3722,7 +3724,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -6672,6 +6673,11 @@ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==" }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -7405,6 +7411,21 @@ "node": ">=10" } }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -16294,6 +16315,23 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, + "node_modules/react-datepicker": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz", + "integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==", + "dependencies": { + "@popperjs/core": "^2.11.8", + "classnames": "^2.2.6", + "date-fns": "^2.30.0", + "prop-types": "^15.7.2", + "react-onclickoutside": "^6.13.0", + "react-popper": "^2.3.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17 || ^18", + "react-dom": "^16.9.0 || ^17 || ^18" + } + }, "node_modules/react-dev-utils": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz", @@ -16446,6 +16484,38 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" }, + "node_modules/react-onclickoutside": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz", + "integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md" + }, + "peerDependencies": { + "react": "^15.5.x || ^16.x || ^17.x || ^18.x", + "react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x" + } + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, + "node_modules/react-popper/node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, "node_modules/react-redux": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.0.0.tgz", @@ -19884,6 +19954,14 @@ "makeerror": "1.0.12" } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", diff --git a/package.json b/package.json index 1f87f93..59fcee5 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,11 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "axios": "^1.6.2", + "date-fns": "^2.30.0", "formik": "^2.4.5", "prop-types": "^15.8.1", "react": "^18.2.0", + "react-datepicker": "^4.24.0", "react-dom": "^18.2.0", "react-icons": "^4.12.0", "react-redux": "^9.0.0", diff --git a/src/App.js b/src/App.js index ba10ea1..a21ea62 100644 --- a/src/App.js +++ b/src/App.js @@ -1,3 +1,4 @@ +import './styles/App.css'; import React from 'react'; import Router from './routes/routes'; diff --git a/src/assets/yellow-background-image-2.png b/src/assets/yellow-background-image-2.png new file mode 100644 index 0000000..07cdd9f Binary files /dev/null and b/src/assets/yellow-background-image-2.png differ diff --git a/src/components/Card/DisplayItemCard.js b/src/components/Card/DisplayItemCard.js index e2563b3..43c5de8 100644 --- a/src/components/Card/DisplayItemCard.js +++ b/src/components/Card/DisplayItemCard.js @@ -50,7 +50,7 @@ const DisplayItemCard = ({ name, imgSrc, amount, description }) => { - Configure + Reserve diff --git a/src/components/Form/FormComponent.js b/src/components/Form/FormComponent.js index 33a02f6..f6e4980 100644 --- a/src/components/Form/FormComponent.js +++ b/src/components/Form/FormComponent.js @@ -26,12 +26,16 @@ FormComponent.propTypes = { email: PropTypes.string.isRequired, password: PropTypes.string.isRequired, confirm_password: PropTypes.string.isRequired, + selectedDate: PropTypes.instanceOf(Date).isRequired, + selectedCity: PropTypes.string.isRequired, }).isRequired, schema: PropTypes.shape({ userName: PropTypes.string.isRequired, email: PropTypes.string.isRequired, password: PropTypes.string.isRequired, confirm_password: PropTypes.string.isRequired, + selectedDate: PropTypes.instanceOf(Date).isRequired, + selectedCity: PropTypes.string.isRequired, }).isRequired, onSubmit: PropTypes.func.isRequired, children: PropTypes.node.isRequired, diff --git a/src/components/Form/FormField.js b/src/components/Form/FormField.js index c60c714..75b0460 100644 --- a/src/components/Form/FormField.js +++ b/src/components/Form/FormField.js @@ -1,20 +1,61 @@ import { useField } from 'formik'; import React from 'react'; +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; import PropTypes from 'prop-types'; import styled from '@emotion/styled'; +export const DateField = ({ + label, name, className, ...props +}) => { + const [field, meta, helpers] = useField(name); + + const handleChange = (date) => { + helpers.setValue(date); + }; + + const handleDatePickerChange = (date) => { + handleChange(date); + }; + + console.log('value fromt eh field', field.value); + return ( + + {label} + helpers.setTouched(true)} /> + {meta.touched && meta.error ? ( + {meta.error} + ) : null} + + ); +}; + +DateField.propTypes = { + id: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + className: PropTypes.string.isRequired, +}; + export const SelectField = ({ - label, lpiSrc, rpiSrc, className, name, id, ...props + label, lpiSrc, rpiSrc, className, name, id, options, ...props }) => { const [field, meta] = useField(name); + console.log('selected city', field.value); return ( {label} - + {options.map((option) => ( + + ))} + {lpiSrc ? : null} {lpiSrc ? : null} @@ -32,6 +73,12 @@ SelectField.propTypes = { className: PropTypes.string.isRequired, name: PropTypes.string.isRequired, id: PropTypes.string.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, }; export const TextInputField = ({ @@ -106,6 +153,29 @@ TextAreaInputField.propTypes = { id: PropTypes.string.isRequired, }; +const DateFieldWrapper = styled.div` + display: flex; + flex-direction: column; + margin-bottom: 1rem; + + label { + margin-bottom: 0.5rem; + } + + .react-datepicker-wrapper, + .react-datepicker__input-container { + width: 100%; + } +`; + +const StyledDatePicker = styled(DatePicker)` + width: 100%; + padding: 0.5rem; + font-size: 1rem; + border: 1px solid #ccc; + border-radius: 4px; +`; + export const InputWrapper = styled.div` width: 100%; `; diff --git a/src/components/MyReservations/MyReservationsList.js b/src/components/MyReservations/MyReservationsList.js index e67dda0..495b2e4 100644 --- a/src/components/MyReservations/MyReservationsList.js +++ b/src/components/MyReservations/MyReservationsList.js @@ -1 +1,35 @@ -// my reservations list component +import React, {useEffect} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchCarReservations } from '../../redux/thunk'; + +const MyReservationsList = () => { + const dispatch = useDispatch(); + const myReservations = useSelector((state) => state.reservation.reservations); + const status = useSelector((state) => state.reservation.status); + const error = useSelector((state) => state.reservation.error); + + useEffect(() => { + dispatch(fetchCarReservations()); + }, [dispatch]); + + if (status === 'loading') { + return

Loading...

; + } + + if (status === 'failed') { + return

Error: {error}

; + } + + return ( +
+

My Reservations

+ {myReservations.map((reservation) => ( +
+

{`Car: ${reservation.car.name}, Date: ${reservation.date}, City: ${reservation.city}`}

+
+ ))} +
+ ); +}; + +export default MyReservationsList; diff --git a/src/components/ReserveCars/ReserveCar.js b/src/components/ReserveCars/ReserveCar.js index 434d303..3dfcbe9 100644 --- a/src/components/ReserveCars/ReserveCar.js +++ b/src/components/ReserveCars/ReserveCar.js @@ -1 +1,28 @@ -// reserve item page component +import React from 'react'; +import ReserveCarFrom from './ReserveCarForm'; + +const ReserveCar = () => ( +
+
+
+

Book A Car on Hourly bases!

+
+

+ You can reserve a car from the list of cars present. You can Book the car by selecting the + {' '} + City + {' '} + and the + Date + {' '} + you want to book + the Vehicle. We have services all over the globe which some include test-riding facilites. + To book the car select both City and Date, please use the selector below. +

+ +
+
+
+); + +export default ReserveCar; diff --git a/src/components/ReserveCars/ReserveCarForm.js b/src/components/ReserveCars/ReserveCarForm.js new file mode 100644 index 0000000..384dc86 --- /dev/null +++ b/src/components/ReserveCars/ReserveCarForm.js @@ -0,0 +1,61 @@ +import React from 'react'; +import { format } from 'date-fns'; +// import { useDispatch } from 'react-redux'; +import FormComponent from '../Form/FormComponent'; +import { ReserveCarSchema, reserveCarInitialValues } from '../../models/reserveCar.model'; +import { DateField, SelectField, TextInputField } from '../Form/FormField'; + +import FormSubmitButton from '../Button/FormSubmitButton'; +// import { reserveCar } from '../../redux/thunk'; + +const ReserveCarFrom = () => { +// const dispatch = useDispatch(); +// const { carId } = useParams(); + + const handleSubmit = (values) => { + const formattedDate = format(values.selectedDate, 'EEE, dd MMM yyyy'); + // const reservationData = { + // userName: values.userName, + // selectedDate: formattedDate, + // selectedCity: values.selectedCity + // } + // dispatch(reserveCar({carId, data: reservationData})); + console.log('Reservation successful'); + console.log(values); + console.log('Picked date is:', formattedDate); + console.log('Picked City is: ', values.selectedCity); + console.log('User name:', values.userName); + }; + + const options = [ + { value: 'City A', label: 'City A' }, + { value: 'City B', label: 'City B' }, + { value: 'City C', label: 'City C' }, + { value: 'City D', label: 'City D' }, + { value: 'City E', label: 'City E' }, + ]; + + const testUsername = 'Fill User Name Boo'; + // const testCarname = 'Fill Car Name Booo' + + return ( + + + + +
+ + + Book Now + +
+
+ ); +}; + +export default ReserveCarFrom; diff --git a/src/layout/LandingPage/nav/navConfig.js b/src/layout/LandingPage/nav/navConfig.js index 9264091..ec32338 100644 --- a/src/layout/LandingPage/nav/navConfig.js +++ b/src/layout/LandingPage/nav/navConfig.js @@ -1,6 +1,6 @@ import { MY_RESERVATIONS, - RESERVED_CARS, + RESERVE_CARS, } from '../../../routes/routeConstants'; const navConfig = [ @@ -10,8 +10,8 @@ const navConfig = [ }, { - title: 'Reserved Cars', - path: RESERVED_CARS, + title: 'Reserve Cars', + path: RESERVE_CARS, }, ]; diff --git a/src/layout/UsersDashboard/sideNav/navConfig.js b/src/layout/UsersDashboard/sideNav/navConfig.js index 043e498..e338f1b 100644 --- a/src/layout/UsersDashboard/sideNav/navConfig.js +++ b/src/layout/UsersDashboard/sideNav/navConfig.js @@ -1,5 +1,5 @@ import { - RESERVED_CARS, + RESERVE_CARS, MY_RESERVATIONS, USERDASHBOARDHOME, ADD_NEW_CAR, @@ -21,8 +21,8 @@ const navConfig = [ }, { - title: 'Reserved Cars', - path: RESERVED_CARS, + title: 'Reserve Cars', + path: RESERVE_CARS, // icon: PlaneIcon(), }, diff --git a/src/models/reserveCar.model.js b/src/models/reserveCar.model.js new file mode 100644 index 0000000..ea290d8 --- /dev/null +++ b/src/models/reserveCar.model.js @@ -0,0 +1,11 @@ +import * as Yup from 'yup'; + +export const ReserveCarSchema = Yup.object().shape({ + selectedDate: Yup.date().required('Date is Required'), + selectedCity: Yup.string().required('City is required'), +}); + +export const reserveCarInitialValues = { + selectedDate: null, + selectedCity: '', +}; diff --git a/src/models/signin.model.js b/src/models/signin.model.js index 93bcc18..5bf9b6f 100644 --- a/src/models/signin.model.js +++ b/src/models/signin.model.js @@ -1,11 +1,11 @@ import * as Yup from 'yup'; export const SignInSchema = Yup.object().shape({ - email: Yup.string().email('Invalid email address').required('Email is required'), + username: Yup.string().required('User name is required'), password: Yup.string().required('Password is required'), }); export const signInInitialValues = { - email: '', + username: '', password: '', }; diff --git a/src/models/signup.model.js b/src/models/signup.model.js index 625f351..6f5a5b6 100644 --- a/src/models/signup.model.js +++ b/src/models/signup.model.js @@ -1,15 +1,15 @@ import * as Yup from 'yup'; export const SignUpSchema = Yup.object().shape({ - userName: Yup.string().required('User Name is required'), + username: Yup.string().required('User Name is required'), email: Yup.string().email('Invalid email address').required('Email is required'), password: Yup.string().min(6, 'Password must be at least 6 characters').required('Password is required'), - confirm_password: Yup.string().oneOf([Yup.ref('password'), null], 'Password must match').required('Confirm the Password'), + password_confirmation: Yup.string().oneOf([Yup.ref('password'), null], 'Password must match').required('Confirm the Password'), }); export const signUpInitialValues = { - userName: '', + username: '', email: '', password: '', - confirm_password: '', + paswword_confirmation: '', }; diff --git a/src/pages/Auth/SignIn/SignIn.js b/src/pages/Auth/SignIn/SignIn.js index 378e442..6380e76 100644 --- a/src/pages/Auth/SignIn/SignIn.js +++ b/src/pages/Auth/SignIn/SignIn.js @@ -2,9 +2,12 @@ import React from 'react'; import SignInForm from './SigninForm'; const SignIn = () => ( -
-

Login Page

- +
+

Sign in

+

Hello there! Sign in and start managing your application

+
+ +
); diff --git a/src/pages/Auth/SignIn/SigninForm.js b/src/pages/Auth/SignIn/SigninForm.js index fbee6d3..445a515 100644 --- a/src/pages/Auth/SignIn/SigninForm.js +++ b/src/pages/Auth/SignIn/SigninForm.js @@ -1,20 +1,22 @@ -import React from 'react'; -import { useDispatch } from 'react-redux'; +import React, { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import FormComponent from '../../../components/Form/FormComponent'; import { SignInSchema, signInInitialValues } from '../../../models/signin.model'; import FormSubmitButton from '../../../components/Button/FormSubmitButton'; import { TextInputField } from '../../../components/Form/FormField'; -// import { useEffect } from 'react'; -import { userLogin } from '../../../redux/thunk'; -// import { useSelector } from 'react-redux'; + import HideableTextFormField from '../../../components/Form/HideableTextFormField'; +import { getAuthenticationToken, loginUser } from '../../../redux/thunk'; const SignInForm = () => { const dispatch = useDispatch(); - // const authenticatedUser = (state => state.authentication.authenticatedUser) + const navigate = useNavigate(); const handleSubmit = (values) => { console.log(values); - dispatch(userLogin(values)); + dispatch(loginUser(values)); + console.log('Login successful'); + navigate('/'); }; return ( @@ -24,7 +26,7 @@ const SignInForm = () => { onSubmit={handleSubmit} className="sign-in-form" > - + Log In diff --git a/src/pages/Auth/SignUp/SignUp.js b/src/pages/Auth/SignUp/SignUp.js index 28ec396..9f48f02 100644 --- a/src/pages/Auth/SignUp/SignUp.js +++ b/src/pages/Auth/SignUp/SignUp.js @@ -1,10 +1,18 @@ import React from 'react'; +import { NavLink } from 'react-router-dom'; import SignUpForm from './SignupForm'; const SignUp = () => ( -
-

Signup page

- +
+

Sign up

+

Hello there! Register and start managing your application

+
+ +
+

Have an existing account ?

+ + Log In +
); diff --git a/src/pages/Auth/SignUp/SignupForm.js b/src/pages/Auth/SignUp/SignupForm.js index 53308a4..596d165 100644 --- a/src/pages/Auth/SignUp/SignupForm.js +++ b/src/pages/Auth/SignUp/SignupForm.js @@ -1,19 +1,20 @@ import React from 'react'; import { useDispatch } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; import FormComponent from '../../../components/Form/FormComponent'; import { SignUpSchema, signUpInitialValues } from '../../../models/signup.model'; import FormSubmitButton from '../../../components/Button/FormSubmitButton'; import { TextInputField } from '../../../components/Form/FormField'; -// import { useEffect } from 'react'; -import { userSignUp } from '../../../redux/thunk'; import HideableTextFormField from '../../../components/Form/HideableTextFormField'; +import { registerUser } from '../../../redux/thunk'; const SignUpForm = () => { const dispatch = useDispatch(); - + const navigate = useNavigate(); const handleSubmit = (values) => { console.log(values); - dispatch(userSignUp(values)); + dispatch(registerUser(values)); + navigate('/'); }; return ( @@ -23,10 +24,10 @@ const SignUpForm = () => { onSubmit={handleSubmit} className="sign-up-form" > - + - + Sign Up diff --git a/src/pages/LandingPage/Home/Home.js b/src/pages/LandingPage/Home/Home.js index b9b633b..c66aa39 100644 --- a/src/pages/LandingPage/Home/Home.js +++ b/src/pages/LandingPage/Home/Home.js @@ -10,7 +10,7 @@ const Home = () => ( Bicycle - THE NEW VESPA TRIDENT + CarBooky diff --git a/src/pages/LandingPage/MyReservation/MyReservation.js b/src/pages/LandingPage/MyReservation/MyReservation.js index f05aff7..cdfd767 100644 --- a/src/pages/LandingPage/MyReservation/MyReservation.js +++ b/src/pages/LandingPage/MyReservation/MyReservation.js @@ -1,5 +1,11 @@ import React from 'react'; +import MyReservationsList from '../../../components/MyReservations/MyReservationsList'; -const MyReservations = () =>

My Reservations

; +const MyReservations = () => ( +
+

My Current Reservations

+ +
+); export default MyReservations; diff --git a/src/pages/LandingPage/ReserveCars/ReserveCars.js b/src/pages/LandingPage/ReserveCars/ReserveCars.js new file mode 100644 index 0000000..c980859 --- /dev/null +++ b/src/pages/LandingPage/ReserveCars/ReserveCars.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReserveCar from '../../../components/ReserveCars/ReserveCar'; + +const ReserveCars = () => ( +
+

Reserve car

+ +
+); + +export default ReserveCars; diff --git a/src/pages/LandingPage/ReservedCars/MyReservation.js b/src/pages/LandingPage/ReservedCars/MyReservation.js deleted file mode 100644 index 1781a90..0000000 --- a/src/pages/LandingPage/ReservedCars/MyReservation.js +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -const Reserved = () =>

My Reservations

; - -export default Reserved; diff --git a/src/pages/UserDashboard/DashboardHome.js b/src/pages/UserDashboard/DashboardHome.js index a86c37b..e781ef9 100644 --- a/src/pages/UserDashboard/DashboardHome.js +++ b/src/pages/UserDashboard/DashboardHome.js @@ -46,7 +46,7 @@ const DashboardHome = () => { <h2>LATEST MODELS</h2> - <p>Please select a Vespa Model</p> + <p>Please select a Vehicle</p> diff --git a/src/redux/authentication/authenticationSlice.js b/src/redux/authentication/authenticationSlice.js index 24364f0..6209695 100644 --- a/src/redux/authentication/authenticationSlice.js +++ b/src/redux/authentication/authenticationSlice.js @@ -1,7 +1,5 @@ import { createSlice } from '@reduxjs/toolkit'; -import { - userSignUp, userLogin, userLogout, fetchUser, -} from '../thunk'; +import { loginUser, logoutUser, registerUser } from '../thunk'; const initialState = { authenticatedUser: {}, @@ -12,49 +10,48 @@ const initialState = { const authenticationSlice = createSlice({ name: 'authenticatedUser', initialState, + reducers: {}, extraReducers: (builder) => { builder - .addCase(userSignUp.pending, (state) => { + .addCase(registerUser.pending, (state) => { state.status = 'loading'; }) - .addCase(userSignUp.fulfilled, (state, action) => { + .addCase(registerUser.fulfilled, (state, action) => { state.authenticatedUser = action.payload.data; - state.status = action.payload.status === 200 ? 'succeeded' : 'failed'; + state.status = action.payload.status === 'succeeded' ? 'succeeded' : 'failed'; }) - .addCase(userSignUp.rejected, (state, action) => { + .addCase(registerUser.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }) - .addCase(userLogin.pending, (state) => { + .addCase(loginUser.pending, (state) => { state.status = 'loading'; - }) - .addCase(userLogin.fulfilled, (state, action) => { - state.authenticatedUser = action.payload.data; - state.status = action.payload.status === 200 ? 'succeeded' : 'failed'; - }) - .addCase(userLogin.rejected, (state, action) => { + console.log('Action staus: blahh is', state.status); + }) + .addCase(loginUser.fulfilled, (state, action) => { + if (action.payload.status === 'failed') { + state.status = 'failed'; + state.error = action.payload.error; + } else { + state.authenticatedUser = action.payload.user; + state.status = action.payload.status || 'succeeded'; + console.log('Action paylod to check if there is a status:', action.payload); + console.log('Authenticated user is: ', state.authenticatedUser); + console.log('Authenticated user action status: ', state.status); + } + }) + .addCase(loginUser.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }) - .addCase(userLogout.pending, (state) => { + .addCase(logoutUser.pending, (state) => { state.status = 'loading'; }) - .addCase(userLogout.fulfilled, (state, action) => { + .addCase(logoutUser.fulfilled, (state, action) => { state.authenticatedUser = {}; state.status = action.payload.status; }) - .addCase(userLogout.rejected, (state, action) => { - state.status = 'failed'; - state.error = action.error.message; - }) - .addCase(fetchUser.pending, (state) => { - state.status = 'loading'; - }) - .addCase(fetchUser.fulfilled, (state, action) => { - state.authenticatedUser = action.payload.data; - state.status = 'succeeded'; - }) - .addCase(fetchUser.rejected, (state, action) => { + .addCase(logoutUser.rejected, (state, action) => { state.status = 'failed'; state.error = action.error.message; }); diff --git a/src/redux/reservations/reservationSlice.js b/src/redux/reservations/reservationSlice.js new file mode 100644 index 0000000..0422aad --- /dev/null +++ b/src/redux/reservations/reservationSlice.js @@ -0,0 +1,44 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { fetchCarReservations, postReserveCar } from '../thunk'; + +const initialState = { + reservation: {}, + reservations: [], + status: 'idle', + error: null, +}; + +const reservationSlice = createSlice({ + name: 'reservation', + initialState, + reducers: {}, + extraReducers: (builder) => { + builder + .addCase(postReserveCar.pending, (state) => { + state.status = 'loading'; + state.error = null; + }) + .addCase(postReserveCar.fulfilled, (state, action) => { + state.status = 'succeeded'; + state.reservation = action.payload.data; + }) + .addCase(postReserveCar.rejected, (state, action) => { + state.status = 'failed'; + state.error = action.error.message; + }) + .addCase(fetchCarReservations.pending, (state) => { + state.status = 'loading'; + state.error = null; + }) + .addCase(fetchCarReservations.fulfilled, (state, action) => { + state.status = 'succeeded'; + state.reservations = action.payload.data; + }) + .addCase(fetchCarReservations.rejected, (state, action) => { + state.status = 'failed'; + state.error = action.error.message; + }); + }, +}); + +export default reservationSlice.reducer; diff --git a/src/redux/store.js b/src/redux/store.js index 4e76c9c..1fa3e5f 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,10 +1,12 @@ import { configureStore } from '@reduxjs/toolkit'; import logger from 'redux-logger'; import authenticationReducer from './authentication/authenticationSlice'; +import reservationReducer from './reservations/reservationSlice'; const store = configureStore({ reducer: { authencation: authenticationReducer, + reservation: reservationReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger), }); diff --git a/src/redux/thunk.js b/src/redux/thunk.js index 28f9dea..8572991 100644 --- a/src/redux/thunk.js +++ b/src/redux/thunk.js @@ -1,73 +1,127 @@ -import { createAsyncThunk } from '@reduxjs/toolkit'; import axios from 'axios'; +import { createAsyncThunk } from '@reduxjs/toolkit'; + +const baseURL = 'http://localhost:4000/api'; -const baseURL = 'http://localhost:3000/api'; +const setAuthenticationToken = ({ headers }) => { + const authorizationHeader = headers.get('authorization'); -const setAuthenticationToken = ({ headers }) => localStorage.setItem('token', headers.get('Authorization')); + if (authorizationHeader) { + localStorage.setItem('token', authorizationHeader); + } +}; + +export const getAuthenticationToken = () => localStorage.getItem('token') || false; const removeAuthenticationToken = () => localStorage.removeItem('token'); -export const userSignUp = createAsyncThunk( - 'users/signup', - async (user, thunkAPI) => { - try { - const response = await axios.post(`${baseURL}/users/sign_up`, user, { - withCredentials: true, - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(user), - }); +const handleResponse = async (response) => { + const { status, data, headers } = response; - setAuthenticationToken(response); + console.log('Response headers: ', headers); - return response.data; - } catch (error) { - return thunkAPI.rejectWithValue(error); - } - }, -); + if (status === 200 || status === 201) { + return { data, status: 'succeeded' }; + } + + if (status === 401 || status === 500) { + removeAuthenticationToken(); + return { status: 'expired', error: 'Unauthorized', message: 'Session has expired' }; + } -export const userLogin = createAsyncThunk( - 'users/login', + return { status: 'failed', error: 'Request failed', message: data.message }; +}; + +export const loginUser = createAsyncThunk( + 'auth/login', async (user, thunkAPI) => { try { - const response = await axios.post(`${baseURL}/users/sign_in`, user); + const response = await axios.post(`${baseURL}/users/sign_in`, { user }); + const { data, headers } = response; + console.log('the data from async method:'); + console.log(data); + console.log('the headers:'); + console.log(headers); - setAuthenticationToken(response); + if (response.status === 200 || response.status === 201) { + setAuthenticationToken({ headers }); + } - return response.data; + return data; } catch (error) { return thunkAPI.rejectWithValue(error); } }, ); -export const userLogout = createAsyncThunk( - 'users/logout', - async (_, thunkAPI) => { - try { - const response = await axios.delete(`${baseURL}/logout`); +export const registerUser = createAsyncThunk('auth/register', async (user, thunkAPI) => { + try { + const response = await axios.post(`${baseURL}/users`, { user }); + const { data, headers } = response; - removeAuthenticationToken(); + if (response.status === 200 || response.status === 201) { + setAuthenticationToken({ headers }); + } - return response.data; - } catch (error) { - return thunkAPI.rejectWithValue(error); + const handledResponse = await handleResponse(response); + return { ...data, ...handledResponse }; + } catch (error) { + return thunkAPI.rejectWithValue(error); + } +}); + +export const logoutUser = createAsyncThunk('auth/logout', async (_, thunkAPI) => { + try { + const response = await axios.delete(`${baseURL}/users`, { + headers: { Authorization: localStorage.getItem('token') }, + }); + + const { status, message } = await handleResponse(response); + + if (status === 'succeeded') { + removeAuthenticationToken(); } - }, -); -export const fetchUser = createAsyncThunk( - 'users/fetchUser', - async (_, thunkAPI) => { - try { - const response = await axios.get(`${baseURL}/users`); + return { status, message }; + } catch (error) { + return thunkAPI.rejectWithValue(error); + } +}); - setAuthenticationToken(response); +export const postReserveCar = createAsyncThunk( + 'reservations/newReserve', + async ({ carId, reservationData }, thunkAPI) => { + try { + const token = localStorage.getItem('token'); + const response = await axios.post(`${baseURL}/car/${carId}/new_reserve`, reservationData, { + headers: { + Authorization: token, + }, + }); + const { data } = await handleResponse(response); - return response.data; + if (response.status === 200 || response.status === 201) { + return { data, status: 'succeeded' }; + } + return { status: 'failed', error: 'Request failed', message: data.message }; } catch (error) { return thunkAPI.rejectWithValue(error); } }, ); + +export const fetchCarReservations = createAsyncThunk( + 'reservations/fetchCarReservations', + async (_, thunkAPI) => { + try { + const token = localStorage.getItem('token'); + const response = await axios.get(`${baseURL}/my_reservations`, { + headers: { + Authorization: token, + }, + }); + return response.data; + } catch (error) { + return thunkAPI.rejectWithValue(error); + } + }, +); diff --git a/src/routes/routeConstants.js b/src/routes/routeConstants.js index 0fe389d..8e181ff 100644 --- a/src/routes/routeConstants.js +++ b/src/routes/routeConstants.js @@ -7,7 +7,7 @@ export const FORGOT_PASSWORD = 'forgot-password'; // Landing-page routes export const HOME = '/'; export const MY_RESERVATIONS = 'my-reservations'; -export const RESERVED_CARS = 'reserved-cars'; +export const RESERVE_CARS = 'reserve-cars'; export const ADD_NEW_CAR = 'add-new-car'; export const DELETE_RESERVATION = 'delete-reservation'; export const CONTACT = 'contact' diff --git a/src/routes/routes.js b/src/routes/routes.js index 85cb409..fc5989e 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -7,7 +7,7 @@ import SignIn from '../pages/Auth/SignIn/SignIn'; import SignUp from '../pages/Auth/SignUp/SignUp'; import NotFound404 from '../pages/NotFound404'; import MyReservations from '../pages/LandingPage/MyReservation/MyReservation'; -import Reserved from '../pages/LandingPage/ReservedCars/MyReservation'; +import ReserveCars from '../pages/LandingPage/ReserveCars/ReserveCars'; import ItemDetail from '../pages/UserDashboard/ItemDetail/ItemDetail'; import Home from '../pages/LandingPage/Home/Home'; import { @@ -17,7 +17,7 @@ import { FORGOT_PASSWORD, MY_RESERVATIONS, HOME, - RESERVED_CARS, + RESERVE_CARS, USERDASHBOARDHOME, USERS_DASHBOARD, ADD_NEW_CAR, @@ -49,7 +49,7 @@ export default function Router() { { path: USERS_DASHBOARD, element: }, { path: USERDASHBOARDHOME, element: }, { path: MY_RESERVATIONS, element: }, - { path: RESERVED_CARS, element: }, + { path: RESERVE_CARS, element: }, { path: ADD_NEW_CAR, element: }, { path: ITEM_DETAIL, element: }, { path: DELETE_RESERVATION, element: }, diff --git a/src/styles/App.css b/src/styles/App.css index cca57b3..743ca5e 100644 --- a/src/styles/App.css +++ b/src/styles/App.css @@ -2,4 +2,67 @@ margin: 0; padding: 0; box-sizing: border-box; + font-family: 'Lato', sans-serif; +} + +.signin-page-outer, +.signup-page-outer, +.my-reservations-page-outer, +.reserve-cars-page-outer { + background: #ffd700; + width: 100%; + height: 100vh; + display: flex; + gap: 2rem; + flex-direction: column; + align-items: center; + justify-content: center; +} + +.reserve-page-title { + text-align: center; +} + +p.user-headline { + text-align: center; + width: 70%; +} + +.reserve-car-outer { + background: url('../assets/yellow-background-image-2.png') center/cover; + height: 100vh; +} + +.reserve-car-inner { + color: #000; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#reserve-car-head-title { + font-size: 2rem; +} + +#reserve-page-head-description { + width: 70%; + text-align: center; + padding: 2rem; +} + +.city-submit-div { + display: flex; + gap: 2rem; + width: 100%; + min-height: 100px; + padding: 20px; +} + +select.css-zdsokt { + border: none; + border-radius: 3rem; + min-width: 150px; }