| @@ -14,7 +14,7 @@ const CartCard = ({ product, initialQuantity, remove, updateQuantity }) => { | |||
| <Paper | |||
| sx={{ | |||
| p: 1, | |||
| width: '88%', | |||
| width: { lg: '88%', xs: '80%' }, | |||
| mb: 2, | |||
| ml: 12, | |||
| backgroundColor: '#f2f2f2', | |||
| @@ -8,7 +8,7 @@ const OrderCard = ({ data }) => { | |||
| elevation={3} | |||
| > | |||
| <Typography sx={{ fontWeight: 600 }}> | |||
| Order placed on {data.date} | |||
| Order placed on: {data.date} | |||
| </Typography> | |||
| <Divider /> | |||
| <Typography sx={{ mt: 1 }}>By: {data.name}</Typography> | |||
| @@ -2,6 +2,7 @@ import { Button, Divider, Paper, Typography } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import Image from 'next/image'; | |||
| import { useRouter } from 'next/router'; | |||
| import { setCookie } from 'nookies'; | |||
| import PropType from 'prop-types'; | |||
| const OrderSummaryCard = ({ data }) => { | |||
| @@ -41,6 +42,11 @@ const OrderSummaryCard = ({ data }) => { | |||
| disabled={data.totalQuantity > 0 ? false : true} | |||
| onClick={() => { | |||
| router.push('/checkout'); | |||
| setCookie(null, 'checkout-session', 'active', { | |||
| maxAge: 3600, | |||
| expires: new Date(Date.now() + 3600), | |||
| path: '/', | |||
| }); | |||
| }} | |||
| > | |||
| Proceed to Checkout | |||
| @@ -1,13 +1,22 @@ | |||
| import { Breadcrumbs, Divider, Grid, Typography } from '@mui/material'; | |||
| import { Grid, Typography } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import { destroyCookie } from 'nookies'; | |||
| import { useEffect } from 'react'; | |||
| import { useStore, useStoreUpdate } from '../../store/cart-context'; | |||
| import CartCard from '../cards/cart-card/CartCard'; | |||
| import OrderSummaryCard from '../cards/order-summary-card/OrderSummaryCard'; | |||
| import StepTitle from '../layout/steps-title/StepTitle'; | |||
| const CartContent = () => { | |||
| const { cartStorage, totalPrice, totalQuantity } = useStore(); | |||
| const { removeCartValue, updateItemQuantity } = useStoreUpdate(); | |||
| useEffect(() => { | |||
| destroyCookie(null, 'checkout-session', { | |||
| path: '/', | |||
| }); | |||
| }, []); | |||
| const mapProductsToDom = () => { | |||
| if (cartStorage?.length) { | |||
| return cartStorage.map((element, i) => ( | |||
| @@ -35,34 +44,17 @@ const CartContent = () => { | |||
| ); | |||
| } | |||
| }; | |||
| return ( | |||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | |||
| <Grid item xs={12}> | |||
| <Typography | |||
| variant="h3" | |||
| sx={{ pl: 12, mt: 12, height: '100%', color: 'primary.main' }} | |||
| > | |||
| Items in Your Cart | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Divider sx={{ backgroundColor: 'primary.main', mx: 12 }} /> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ mt: 4 }}> | |||
| <Breadcrumbs | |||
| aria-label="breadcrumb" | |||
| separator="›" | |||
| sx={{ pl: 12, fontSize: 20 }} | |||
| > | |||
| <Typography color="red">Cart</Typography> | |||
| <Typography></Typography> | |||
| </Breadcrumbs> | |||
| </Grid> | |||
| <Grid item xs={8}> | |||
| <StepTitle title="Items in Your Cart" breadcrumbsArray={['Cart']} /> | |||
| <Grid item lg={8} xs={12} sx={{ mt: 2 }}> | |||
| {mapProductsToDom()} | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <Box sx={{ width: '80%', mt: 2 }}> | |||
| <Grid item lg={4} xs={12}> | |||
| <Box | |||
| sx={{ width: { xs: '90%', lg: '80%' }, mt: 2, pl: { xs: 12, lg: 0 } }} | |||
| > | |||
| <OrderSummaryCard | |||
| data={{ totalPrice: totalPrice, totalQuantity: totalQuantity }} | |||
| ></OrderSummaryCard> | |||
| @@ -1,15 +1,18 @@ | |||
| import { Breadcrumbs, Divider, Grid, Typography } from '@mui/material'; | |||
| import { Grid, Typography } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import { useSession } from 'next-auth/react'; | |||
| import { useRouter } from 'next/router'; | |||
| import { setCookie } from 'nookies'; | |||
| import { useStore } from '../../store/cart-context'; | |||
| import { useCheckoutDataUpdate } from '../../store/checkout-context'; | |||
| import DataCard from '../cards/data-card/DataCard'; | |||
| import ShippingDetailsForm from '../forms/shipping-details/ShippingDetailsForm'; | |||
| import StepTitle from '../layout/steps-title/StepTitle'; | |||
| const CheckoutContent = () => { | |||
| const { cartStorage } = useStore(); | |||
| const { addCheckoutValue } = useCheckoutDataUpdate(); | |||
| const { data: session } = useSession(); | |||
| const router = useRouter(); | |||
| @@ -19,32 +22,30 @@ const CheckoutContent = () => { | |||
| { ...formValues, email: session.user.email }, | |||
| session.user._id | |||
| ); | |||
| setCookie(null, 'shipping-session', 'active', { | |||
| maxAge: 3600, | |||
| expires: new Date(Date.now() + 3600), | |||
| path: '/', | |||
| }); | |||
| router.push('/shipping'); | |||
| }; | |||
| const mapProductsToDom = () => { | |||
| return cartStorage?.map((entry, i) => ( | |||
| <DataCard | |||
| key={i} | |||
| data={entry.product} | |||
| quantity={entry.quantity} | |||
| ></DataCard> | |||
| )); | |||
| }; | |||
| return ( | |||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | |||
| <Grid item xs={12}> | |||
| <Typography | |||
| variant="h3" | |||
| sx={{ pl: 12, mt: 12, height: '100%', color: 'primary.main' }} | |||
| > | |||
| Checkout | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Divider sx={{ backgroundColor: 'primary.main', mx: 12 }} /> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ mt: 4 }}> | |||
| <Breadcrumbs | |||
| aria-label="breadcrumb" | |||
| separator="›" | |||
| sx={{ pl: 12, fontSize: 20 }} | |||
| > | |||
| <Typography>Cart</Typography> | |||
| <Typography color="red">Checkout</Typography> | |||
| </Breadcrumbs> | |||
| </Grid> | |||
| <StepTitle | |||
| title="Items in Your Cart" | |||
| breadcrumbsArray={['Cart', 'Checkout']} | |||
| /> | |||
| <Grid item xs={12} sx={{ mt: 1 }}> | |||
| <Typography sx={{ pl: 12, fontSize: 20 }}> | |||
| The following fields will be used as the shipping details for your | |||
| @@ -59,17 +60,7 @@ const CheckoutContent = () => { | |||
| ></ShippingDetailsForm> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <Box sx={{ width: '80%', mt: 2 }}> | |||
| {cartStorage?.map((entry, i) => { | |||
| return ( | |||
| <DataCard | |||
| key={i} | |||
| data={entry.product} | |||
| quantity={entry.quantity} | |||
| ></DataCard> | |||
| ); | |||
| })} | |||
| </Box> | |||
| <Box sx={{ width: '80%', mt: 2 }}>{mapProductsToDom()}</Box> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| @@ -1,118 +1,80 @@ | |||
| import { | |||
| Box, | |||
| Button, | |||
| Container, | |||
| Grid, | |||
| TextField, | |||
| Typography | |||
| } from '@mui/material'; | |||
| import { Box, Button, Paper, TextField } from '@mui/material'; | |||
| import { useFormik } from 'formik'; | |||
| import { useTranslation } from 'next-i18next'; | |||
| import Link from 'next/link'; | |||
| import React from 'react'; | |||
| import { BASE_PAGE } from '../../../constants/pages'; | |||
| import PropType from 'prop-types'; | |||
| import React, { useState } from 'react'; | |||
| import { contactSchema } from '../../../schemas/contactSchema'; | |||
| const ContactForm = () => { | |||
| const { t } = useTranslation('forms', 'contact', 'common'); | |||
| const handleSubmit = (values) => { | |||
| console.log('Values', values); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| firstName: '', | |||
| lastName: '', | |||
| email: '', | |||
| message: '' | |||
| }, | |||
| validationSchema: contactSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Container component="main" maxWidth="md"> | |||
| import { useCheckoutData } from '../../../store/checkout-context'; | |||
| import ErrorMessageComponent from '../../mui/ErrorMessageComponent'; | |||
| const ContactForm = ({ submitHandler }) => { | |||
| const [error] = useState({ hasError: false, errorMessage: '' }); | |||
| const { checkoutStorage } = useCheckoutData(); | |||
| const handleSubmit = async (values) => { | |||
| submitHandler(values.email); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| email: checkoutStorage ? checkoutStorage.userInfo.email : '', | |||
| }, | |||
| validationSchema: contactSchema, | |||
| onSubmit: handleSubmit, | |||
| validateOnBlur: true, | |||
| enableReinitialize: true, | |||
| }); | |||
| return ( | |||
| <Paper | |||
| sx={{ p: 3, width: '90%', ml: 12, mt: 2, backgroundColor: '#f2f2f2' }} | |||
| elevation={3} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| width: '100%', | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| }} | |||
| > | |||
| {error.hasError && <ErrorMessageComponent error={error.errorMessage} />} | |||
| <Box | |||
| sx={{ | |||
| marginTop: 32, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| alignItems: 'center', | |||
| }} | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <Typography component="h1" variant="h5"> | |||
| {t('contact:Title')} | |||
| </Typography> | |||
| <Box | |||
| component="form" | |||
| onSubmit={formik.handleSubmit} | |||
| sx={{ position: 'relative', mt: 1, p: 1 }} | |||
| > | |||
| <TextField | |||
| name="firstName" | |||
| label={t('forms:FirstName')} | |||
| margin="normal" | |||
| value={formik.values.firstName} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.firstName && Boolean(formik.errors.firstName)} | |||
| helperText={formik.touched.firstName && formik.errors.firstName} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="lastName" | |||
| label={t('forms:LastName')} | |||
| margin="normal" | |||
| value={formik.values.lastName} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.lastName && Boolean(formik.errors.lastName)} | |||
| helperText={formik.touched.lastName && formik.errors.lastName} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| <TextField | |||
| name="email" | |||
| label={t('forms:Email')} | |||
| label="Email" | |||
| margin="normal" | |||
| value={formik.values.email} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.email && Boolean(formik.errors.email)} | |||
| helperText={formik.touched.email && formik.errors.email} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <TextField | |||
| name="message" | |||
| label={t('forms:Message')} | |||
| multiline | |||
| margin="normal" | |||
| value={formik.values.message} | |||
| onChange={formik.handleChange} | |||
| error={formik.touched.message && Boolean(formik.errors.message)} | |||
| helperText={formik.touched.message && formik.errors.message} | |||
| rows={4} | |||
| autoFocus | |||
| fullWidth | |||
| /> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ mt: 3, mb: 2 }} | |||
| fullWidth | |||
| > | |||
| {t('contact:SendBtn')} | |||
| </Button> | |||
| <Grid container justifyContent="center"> | |||
| <Link href={BASE_PAGE}>{t('common:Back')}</Link> | |||
| </Grid> | |||
| </Box> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ | |||
| mt: 3, | |||
| mb: 2, | |||
| backgroundColor: '#CBA213', | |||
| height: 50, | |||
| width: 150, | |||
| textTransform: 'none', | |||
| color: 'white', | |||
| }} | |||
| > | |||
| Submit Details | |||
| </Button> | |||
| </Box> | |||
| </Container> | |||
| ); | |||
| }; | |||
| export default ContactForm; | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| ContactForm.propTypes = { | |||
| submitHandler: PropType.func, | |||
| }; | |||
| export default ContactForm; | |||
| @@ -10,23 +10,23 @@ const ShippingDetailsForm = ({ | |||
| backBtn = false, | |||
| isCheckout = false, | |||
| submitHandler, | |||
| enableBtn = true, | |||
| }) => { | |||
| const [error] = useState({ hasError: false, errorMessage: '' }); | |||
| const { data: session } = useSession(); | |||
| const formikSubmitHandler = async (values) => { | |||
| console.log('hi'); | |||
| submitHandler(values); | |||
| }; | |||
| const formik = useFormik({ | |||
| initialValues: { | |||
| fullName: session ? session.user.fullName : '', | |||
| address: session ? session.user.address : '', | |||
| address2: session ? session.user.address2 : '', | |||
| city: session ? session.user.city : '', | |||
| country: session ? session.user.country : '', | |||
| postcode: session ? session.user.postcode : '', | |||
| fullName: session?.user ? session.user.fullName : '', | |||
| address: session?.user ? session.user.address : '', | |||
| address2: session?.user ? session.user.address2 : '', | |||
| city: session?.user ? session.user.city : '', | |||
| country: session?.user ? session.user.country : '', | |||
| postcode: session?.user ? session.user.postcode : '', | |||
| }, | |||
| validationSchema: shippingDetailsSchema, | |||
| onSubmit: formikSubmitHandler, | |||
| @@ -145,6 +145,7 @@ const ShippingDetailsForm = ({ | |||
| textTransform: 'none', | |||
| color: 'white', | |||
| }} | |||
| disabled={!enableBtn} | |||
| onClick={() => { | |||
| submitHandler; | |||
| }} | |||
| @@ -1,6 +1,7 @@ | |||
| import AppBar from '@mui/material/AppBar'; | |||
| import Box from '@mui/material/Box'; | |||
| import Typography from '@mui/material/Typography'; | |||
| import { signOut, useSession } from 'next-auth/react'; | |||
| import Image from 'next/image'; | |||
| import Link from 'next/link'; | |||
| import { useRouter } from 'next/router'; | |||
| @@ -15,6 +16,13 @@ import { useStore } from '../../../store/cart-context'; | |||
| const Navbar = () => { | |||
| const router = useRouter(); | |||
| const { totalQuantity } = useStore(); | |||
| const { data: session } = useSession(); | |||
| const signOutHandler = async () => { | |||
| const data = await signOut({ redirect: false, callbackUrl: '/' }); | |||
| router.push(data.url); | |||
| }; | |||
| return ( | |||
| <AppBar | |||
| position="absolute" | |||
| @@ -128,6 +136,23 @@ const Navbar = () => { | |||
| mr: 4, | |||
| }} | |||
| > | |||
| {session?.user?._id && ( | |||
| <Box | |||
| sx={{ | |||
| mx: 2, | |||
| mt: 0.1, | |||
| cursor: 'pointer', | |||
| }} | |||
| onClick={signOutHandler} | |||
| > | |||
| <Image | |||
| src="/images/logout.svg" | |||
| alt="profile" | |||
| width={18} | |||
| height={20} | |||
| /> | |||
| </Box> | |||
| )} | |||
| <Box | |||
| sx={{ | |||
| mx: 2, | |||
| @@ -178,7 +203,6 @@ const Navbar = () => { | |||
| /> | |||
| </Box> | |||
| </Link> | |||
| , | |||
| </Box> | |||
| </Box> | |||
| </Box> | |||
| @@ -0,0 +1,46 @@ | |||
| import { Breadcrumbs, Divider, Grid, Typography } from '@mui/material'; | |||
| import PropType from 'prop-types'; | |||
| const StepTitle = ({ title, breadcrumbsArray }) => { | |||
| return ( | |||
| <> | |||
| <Grid item xs={12}> | |||
| <Typography | |||
| variant="h3" | |||
| sx={{ pl: 12, mt: 12, height: '100%', color: 'primary.main' }} | |||
| > | |||
| {title} | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Divider sx={{ backgroundColor: 'primary.main', mx: 12 }} /> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ mt: 4 }}> | |||
| <Breadcrumbs | |||
| aria-label="breadcrumb" | |||
| separator="›" | |||
| sx={{ pl: 12, fontSize: 20 }} | |||
| > | |||
| {breadcrumbsArray.map((entry, index) => { | |||
| return ( | |||
| <Typography | |||
| key={index} | |||
| color={index === breadcrumbsArray.length - 1 ? 'red' : 'black'} | |||
| > | |||
| {entry} | |||
| </Typography> | |||
| ); | |||
| })} | |||
| <Typography></Typography> | |||
| </Breadcrumbs> | |||
| </Grid> | |||
| </> | |||
| ); | |||
| }; | |||
| StepTitle.propTypes = { | |||
| title: PropType.string, | |||
| breadcrumbsArray: PropType.arrayOf(PropType.string), | |||
| }; | |||
| export default StepTitle; | |||
| @@ -0,0 +1,7 @@ | |||
| const { CircularProgress } = require('@mui/material'); | |||
| const LoadingSpinner = () => { | |||
| return <CircularProgress />; | |||
| }; | |||
| export default LoadingSpinner; | |||
| @@ -0,0 +1,34 @@ | |||
| import { GoogleMap, Marker, useLoadScript } from '@react-google-maps/api'; | |||
| import LoadingSpinner from '../loader/basic-spinner/LoadSpinner'; | |||
| import { center, libraries, mapContainerStyle } from './MapConst'; | |||
| const Map = () => { | |||
| const { isLoaded, loadError } = useLoadScript({ | |||
| googleMapsApiKey: `${process.env.NEXT_PUBLIC_MAP_KEY}`, | |||
| libraries, | |||
| }); | |||
| let content = ( | |||
| <GoogleMap | |||
| id="map" | |||
| mapContainerStyle={mapContainerStyle} | |||
| zoom={14} | |||
| center={center} | |||
| > | |||
| <Marker | |||
| key={`${center.lat - center.lng}`} | |||
| position={{ | |||
| lat: center.lat, | |||
| lng: center.lng, | |||
| }} | |||
| /> | |||
| </GoogleMap> | |||
| ); | |||
| if (loadError) return 'Error loading map'; | |||
| if (!isLoaded) content = <LoadingSpinner />; | |||
| return <>{content}</>; | |||
| }; | |||
| export default Map; | |||
| @@ -0,0 +1,10 @@ | |||
| export const libraries = ['places']; | |||
| export const mapContainerStyle = { | |||
| width: '100%', | |||
| height: '100%', | |||
| }; | |||
| export const center = { | |||
| lat: 43.30920996410931, | |||
| lng: 21.911334213495593, | |||
| }; | |||
| @@ -1,9 +1,40 @@ | |||
| import { Grid, Typography } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import { signOut, useSession } from 'next-auth/react'; | |||
| import { useState } from 'react'; | |||
| import { updateUser } from '../../requests/user/userUpdateRequest'; | |||
| import OrderCard from '../cards/order-card/OrderCard'; | |||
| import ShippingDetailsForm from '../forms/shipping-details/ShippingDetailsForm'; | |||
| const ProfileContent = () => { | |||
| const ProfileContent = ({ orders }) => { | |||
| const { data: session } = useSession(); | |||
| const [enableBtn, setEnableBtn] = useState(true); | |||
| const updateUserHandler = async (values) => { | |||
| try { | |||
| setEnableBtn(false); | |||
| await updateUser(values, session.user._id); | |||
| signOut(); | |||
| } catch (error) { | |||
| console.log(error); | |||
| setTimeout(() => { | |||
| setEnableBtn(true); | |||
| }, 3000); | |||
| } | |||
| }; | |||
| const mapOrdersToDom = () => | |||
| orders.slice(-4).map((order, i) => ( | |||
| <OrderCard | |||
| key={i} | |||
| data={{ | |||
| date: order.time.split('T')[0], | |||
| name: order.shippingAddress.fullName, | |||
| totalPrice: order.totalPrice, | |||
| }} | |||
| ></OrderCard> | |||
| )); | |||
| return ( | |||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | |||
| <Grid item xs={12}> | |||
| @@ -16,27 +47,20 @@ const ProfileContent = () => { | |||
| </Grid> | |||
| <Grid item xs={8} sx={{ mt: 4 }}> | |||
| <Typography sx={{ pl: 12, fontSize: 20 }}> | |||
| Save details for later | |||
| Save details for later (user will be logged out) | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={4} sx={{ mt: 4 }}> | |||
| <Typography sx={{ fontSize: 20 }}>Previous Orders</Typography> | |||
| </Grid> | |||
| <Grid item xs={8}> | |||
| <ShippingDetailsForm></ShippingDetailsForm> | |||
| <ShippingDetailsForm | |||
| submitHandler={updateUserHandler} | |||
| enableBtn={enableBtn} | |||
| ></ShippingDetailsForm> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <Box sx={{ width: '60%', mt: 2 }}> | |||
| <OrderCard | |||
| data={{ date: '2022-09-02', name: 'John Doe', totalPrice: 30 }} | |||
| ></OrderCard> | |||
| <OrderCard | |||
| data={{ date: '2022-09-02', name: 'John Doe', totalPrice: 30 }} | |||
| ></OrderCard> | |||
| <OrderCard | |||
| data={{ date: '2022-09-02', name: 'John Doe', totalPrice: 30 }} | |||
| ></OrderCard> | |||
| </Box> | |||
| <Box sx={{ width: '60%', mt: 2 }}>{mapOrdersToDom()}</Box> | |||
| </Grid> | |||
| </Grid> | |||
| ); | |||
| @@ -1,33 +1,48 @@ | |||
| import { Breadcrumbs, Button, Divider, Grid, Typography } from '@mui/material'; | |||
| import { Button, Grid, Typography } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import { useRouter } from 'next/router'; | |||
| import { destroyCookie } from 'nookies'; | |||
| import { useEffect, useState } from 'react'; | |||
| import { postOrder } from '../../requests/products/postOrderRequest'; | |||
| import { useStoreUpdate } from '../../store/cart-context'; | |||
| import { useCheckoutDataUpdate } from '../../store/checkout-context'; | |||
| import StepTitle from '../layout/steps-title/StepTitle'; | |||
| let initialRender = true; | |||
| const ReviewContent = () => { | |||
| const { parseCheckoutValue, clearCheckout } = useCheckoutDataUpdate(); | |||
| const { clearCart } = useStoreUpdate(); | |||
| const [orderData, setOrderData] = useState(parseCheckoutValue()); | |||
| const router = useRouter(); | |||
| useEffect(() => { | |||
| if (initialRender) { | |||
| postOrder(orderData); | |||
| initialRender = false; | |||
| return () => { | |||
| clearCheckout(); | |||
| clearCart(); | |||
| destroyCookie(null, 'checkout-session', { | |||
| path: '/', | |||
| }); | |||
| destroyCookie(null, 'shipping-session', { | |||
| path: '/', | |||
| }); | |||
| destroyCookie(null, 'review-session', { | |||
| path: '/', | |||
| }); | |||
| }; | |||
| } | |||
| }, []); | |||
| return ( | |||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | |||
| <Grid item xs={12}> | |||
| <Typography | |||
| variant="h3" | |||
| sx={{ pl: 12, mt: 12, height: '100%', color: 'primary.main' }} | |||
| > | |||
| Shipping | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Divider sx={{ backgroundColor: 'primary.main', mx: 12 }} /> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ mt: 4 }}> | |||
| <Breadcrumbs | |||
| aria-label="breadcrumb" | |||
| separator="›" | |||
| sx={{ pl: 12, fontSize: 20 }} | |||
| > | |||
| <Typography>Cart</Typography> | |||
| <Typography>Checkout</Typography> | |||
| <Typography>Shipping</Typography> | |||
| <Typography>Payment</Typography> | |||
| <Typography color="red">Review</Typography> | |||
| </Breadcrumbs> | |||
| </Grid> | |||
| <StepTitle | |||
| title="Review" | |||
| breadcrumbsArray={['Cart', 'Checkout', 'Shipping', 'Payment', 'Review']} | |||
| /> | |||
| <Grid item xs={12} sx={{ mt: 1 }}> | |||
| <Typography | |||
| sx={{ | |||
| @@ -79,7 +94,7 @@ const ReviewContent = () => { | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | |||
| Order placed on: 05/09/2022 | |||
| Order placed on: {orderData.time} | |||
| </Typography> | |||
| </Box> | |||
| </Grid> | |||
| @@ -94,7 +109,7 @@ const ReviewContent = () => { | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | |||
| Email: johndoe@test | |||
| Email: {orderData?.shippingAddress?.email} | |||
| </Typography> | |||
| </Box> | |||
| </Grid> | |||
| @@ -109,7 +124,7 @@ const ReviewContent = () => { | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | |||
| Total: $60 | |||
| Total: ${orderData?.totalPrice} | |||
| </Typography> | |||
| </Box> | |||
| </Grid> | |||
| @@ -124,8 +139,10 @@ const ReviewContent = () => { | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | |||
| Shipping Address: 1684 Upton Avenue, Locke Mills, United Kingdom, | |||
| 04255 | |||
| Shipping Address: {orderData?.shippingAddress?.address},{' '} | |||
| {orderData?.shippingAddress?.city},{' '} | |||
| {orderData?.shippingAddress?.country},{' '} | |||
| {orderData?.shippingAddress?.postcode} | |||
| </Typography> | |||
| </Box> | |||
| </Grid> | |||
| @@ -153,6 +170,9 @@ const ReviewContent = () => { | |||
| mr: 2, | |||
| fontSize: 16, | |||
| }} | |||
| onClick={() => { | |||
| router.push('/'); | |||
| }} | |||
| > | |||
| Back to Home | |||
| </Button> | |||
| @@ -1,42 +1,75 @@ | |||
| import { | |||
| Breadcrumbs, | |||
| Button, | |||
| Checkbox, | |||
| Divider, | |||
| FormControlLabel, | |||
| Grid, | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { Checkbox, FormControlLabel, Grid, Typography } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import { useCheckoutData } from '../../store/checkout-context'; | |||
| import { useRouter } from 'next/router'; | |||
| import { setCookie } from 'nookies'; | |||
| import { useState } from 'react'; | |||
| import { | |||
| useCheckoutData, | |||
| useCheckoutDataUpdate, | |||
| } from '../../store/checkout-context'; | |||
| import { stripe } from '../../utils/helpers/stripe'; | |||
| import DataCard from '../cards/data-card/DataCard'; | |||
| import StepTitle from '../layout/steps-title/StepTitle'; | |||
| import ButtonGroup from './shipping-btnGroup/ButtonGroup'; | |||
| import ShippingData from './shipping-data/ShippingData'; | |||
| import ShippingModal from './shipping-modal/ShippingModal'; | |||
| const ShippingContent = () => { | |||
| const { checkoutStorage } = useCheckoutData(); | |||
| const { changeContact, changeShippingData } = useCheckoutDataUpdate(); | |||
| const [open, setOpen] = useState({ isOpen: false, type: '' }); | |||
| const router = useRouter(); | |||
| const handleOpen = (type) => setOpen({ isOpen: true, type }); | |||
| const handleClose = () => setOpen({ isOpen: false, type: '' }); | |||
| const handleChangeShipping = (values) => { | |||
| changeShippingData(values); | |||
| handleClose(); | |||
| }; | |||
| const handleChangeContact = (values) => { | |||
| changeContact(values); | |||
| handleClose(); | |||
| }; | |||
| const handleStripePayment = () => { | |||
| stripe({ | |||
| lineItems: [ | |||
| { | |||
| price: 'price_1Lg4MsDY7dvAcw2f1CGQaFFR', | |||
| quantity: 1, | |||
| }, | |||
| ], | |||
| }); | |||
| setCookie(null, 'review-session', 'active', { | |||
| maxAge: 3600, | |||
| expires: new Date(Date.now() + 3600), | |||
| path: '/', | |||
| }); | |||
| }; | |||
| const handleBackToCart = () => { | |||
| router.replace('/cart'); | |||
| }; | |||
| const mapProductsToDom = () => { | |||
| return checkoutStorage?.products?.map((entry, i) => ( | |||
| <DataCard | |||
| key={i} | |||
| data={entry.product} | |||
| quantity={entry.quantity} | |||
| ></DataCard> | |||
| )); | |||
| }; | |||
| return ( | |||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | |||
| <Grid item xs={12}> | |||
| <Typography | |||
| variant="h3" | |||
| sx={{ pl: 12, mt: 12, height: '100%', color: 'primary.main' }} | |||
| > | |||
| Shipping | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={12}> | |||
| <Divider sx={{ backgroundColor: 'primary.main', mx: 12 }} /> | |||
| </Grid> | |||
| <Grid item xs={12} sx={{ mt: 4 }}> | |||
| <Breadcrumbs | |||
| aria-label="breadcrumb" | |||
| separator="›" | |||
| sx={{ pl: 12, fontSize: 20 }} | |||
| > | |||
| <Typography>Cart</Typography> | |||
| <Typography>Checkout</Typography> | |||
| <Typography color="red">Shipping</Typography> | |||
| </Breadcrumbs> | |||
| </Grid> | |||
| <StepTitle | |||
| title="Shipping" | |||
| breadcrumbsArray={['Cart', 'Checkout', 'Shipping']} | |||
| /> | |||
| <Grid item xs={12} sx={{ mt: 1 }}> | |||
| <Typography sx={{ pl: 12, fontSize: 20 }}> | |||
| The following fields will be used as the shipping details for your | |||
| @@ -44,72 +77,13 @@ const ShippingContent = () => { | |||
| </Typography> | |||
| </Grid> | |||
| <Grid item xs={8}> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| backgroundColor: '#f2f2f2', | |||
| alignItems: 'center', | |||
| mt: 2, | |||
| ml: 12, | |||
| mb: 2, | |||
| width: '90%', | |||
| borderRadius: 2, | |||
| p: 1, | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | |||
| Contact | |||
| </Typography> | |||
| <Typography>{checkoutStorage?.userInfo.email}</Typography> | |||
| <Button | |||
| sx={{ | |||
| height: 35, | |||
| width: 125, | |||
| fontSize: 15, | |||
| textTransform: 'none', | |||
| backgroundColor: '#CBA213', | |||
| color: 'white', | |||
| }} | |||
| > | |||
| Change | |||
| </Button> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| backgroundColor: '#f2f2f2', | |||
| alignItems: 'center', | |||
| ml: 12, | |||
| mb: 2, | |||
| width: '90%', | |||
| borderRadius: 2, | |||
| p: 1, | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | |||
| Shipping to | |||
| </Typography> | |||
| <Typography> | |||
| {checkoutStorage?.userInfo.address} |{' '} | |||
| {checkoutStorage?.userInfo.city}{' '} | |||
| {checkoutStorage?.userInfo.postcode} | |||
| </Typography> | |||
| <Button | |||
| sx={{ | |||
| height: 35, | |||
| width: 125, | |||
| fontSize: 15, | |||
| textTransform: 'none', | |||
| backgroundColor: '#CBA213', | |||
| color: 'white', | |||
| }} | |||
| > | |||
| Change | |||
| </Button> | |||
| </Box> | |||
| <ShippingData | |||
| email={checkoutStorage?.userInfo?.email} | |||
| address={checkoutStorage?.userInfo?.address} | |||
| city={checkoutStorage?.userInfo?.city} | |||
| postcode={checkoutStorage?.userInfo?.postcode} | |||
| handleOpen={handleOpen} | |||
| /> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| @@ -129,60 +103,20 @@ const ShippingContent = () => { | |||
| sx={{ color: 'black', ml: 2 }} | |||
| /> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| ml: 12, | |||
| mb: 2, | |||
| borderRadius: 2, | |||
| p: 1, | |||
| }} | |||
| > | |||
| <Button | |||
| variant="contained" | |||
| sx={{ | |||
| mt: 3, | |||
| mb: 2, | |||
| height: 50, | |||
| width: 150, | |||
| textTransform: 'none', | |||
| backgroundColor: 'primary.main', | |||
| color: 'white', | |||
| mr: 2, | |||
| }} | |||
| > | |||
| Back to cart | |||
| </Button> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ | |||
| mt: 3, | |||
| mb: 2, | |||
| backgroundColor: '#CBA213', | |||
| height: 50, | |||
| width: 200, | |||
| textTransform: 'none', | |||
| color: 'white', | |||
| }} | |||
| > | |||
| Continue to payment | |||
| </Button> | |||
| </Box> | |||
| <ButtonGroup | |||
| handleStripePayment={handleStripePayment} | |||
| handleBackToCart={handleBackToCart} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={4}> | |||
| <Box sx={{ width: '80%', mt: 2 }}> | |||
| {checkoutStorage?.products.map((entry, i) => { | |||
| return ( | |||
| <DataCard | |||
| key={i} | |||
| data={entry.product} | |||
| quantity={entry.quantity} | |||
| ></DataCard> | |||
| ); | |||
| })} | |||
| </Box> | |||
| <Box sx={{ width: '80%', mt: 2 }}>{mapProductsToDom()}</Box> | |||
| </Grid> | |||
| <ShippingModal | |||
| open={open} | |||
| handleClose={handleClose} | |||
| handleChangeShipping={handleChangeShipping} | |||
| handleChangeContact={handleChangeContact} | |||
| /> | |||
| </Grid> | |||
| ); | |||
| }; | |||
| @@ -0,0 +1,56 @@ | |||
| import { Box, Button } from '@mui/material'; | |||
| import PropType from 'prop-types'; | |||
| const ButtonGroup = ({ handleBackToCart, handleStripePayment }) => { | |||
| return ( | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| ml: 12, | |||
| mb: 2, | |||
| borderRadius: 2, | |||
| p: 1, | |||
| }} | |||
| > | |||
| <Button | |||
| variant="contained" | |||
| sx={{ | |||
| mt: 3, | |||
| mb: 2, | |||
| height: 50, | |||
| width: 150, | |||
| textTransform: 'none', | |||
| backgroundColor: 'primary.main', | |||
| color: 'white', | |||
| mr: 2, | |||
| }} | |||
| onClick={handleBackToCart} | |||
| > | |||
| Back to cart | |||
| </Button> | |||
| <Button | |||
| type="submit" | |||
| variant="contained" | |||
| sx={{ | |||
| mt: 3, | |||
| mb: 2, | |||
| backgroundColor: '#CBA213', | |||
| height: 50, | |||
| width: 200, | |||
| textTransform: 'none', | |||
| color: 'white', | |||
| }} | |||
| onClick={handleStripePayment} | |||
| > | |||
| Continue to payment | |||
| </Button> | |||
| </Box> | |||
| ); | |||
| }; | |||
| ButtonGroup.propTypes = { | |||
| handleBackToCart: PropType.func, | |||
| handleStripePayment: PropType.func, | |||
| }; | |||
| export default ButtonGroup; | |||
| @@ -0,0 +1,88 @@ | |||
| import { Button, Typography } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import PropType from 'prop-types'; | |||
| const ShippingData = ({ email, address, city, postcode, handleOpen }) => { | |||
| return ( | |||
| <> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| backgroundColor: '#f2f2f2', | |||
| alignItems: 'center', | |||
| mt: 2, | |||
| ml: 12, | |||
| mb: 2, | |||
| width: '90%', | |||
| borderRadius: 2, | |||
| p: 1, | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}>Contact</Typography> | |||
| <Typography>{email}</Typography> | |||
| <Button | |||
| sx={{ | |||
| height: 35, | |||
| width: 125, | |||
| fontSize: 15, | |||
| textTransform: 'none', | |||
| backgroundColor: '#CBA213', | |||
| color: 'white', | |||
| }} | |||
| onClick={() => { | |||
| handleOpen('Contact'); | |||
| }} | |||
| > | |||
| Change | |||
| </Button> | |||
| </Box> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| backgroundColor: '#f2f2f2', | |||
| alignItems: 'center', | |||
| ml: 12, | |||
| mb: 2, | |||
| width: '90%', | |||
| borderRadius: 2, | |||
| p: 1, | |||
| }} | |||
| > | |||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | |||
| Shipping to | |||
| </Typography> | |||
| <Typography> | |||
| {address} | {city} | {postcode} | |||
| </Typography> | |||
| <Button | |||
| sx={{ | |||
| height: 35, | |||
| width: 125, | |||
| fontSize: 15, | |||
| textTransform: 'none', | |||
| backgroundColor: '#CBA213', | |||
| color: 'white', | |||
| }} | |||
| onClick={() => { | |||
| handleOpen('Shipping'); | |||
| }} | |||
| > | |||
| Change | |||
| </Button> | |||
| </Box> | |||
| </> | |||
| ); | |||
| }; | |||
| ShippingData.propTypes = { | |||
| email: PropType.string, | |||
| address: PropType.string, | |||
| city: PropType.string, | |||
| postcode: PropType.string, | |||
| handleOpen: PropType.func, | |||
| }; | |||
| export default ShippingData; | |||
| @@ -0,0 +1,47 @@ | |||
| import { Modal } from '@mui/material'; | |||
| import { Box } from '@mui/system'; | |||
| import PropType from 'prop-types'; | |||
| import ContactForm from '../../forms/contact/ContactForm'; | |||
| import ShippingDetailsForm from '../../forms/shipping-details/ShippingDetailsForm'; | |||
| const ShippingModal = ({ | |||
| open, | |||
| handleClose, | |||
| handleChangeShipping, | |||
| handleChangeContact, | |||
| }) => { | |||
| return ( | |||
| <Modal | |||
| open={open.isOpen} | |||
| onClose={handleClose} | |||
| aria-labelledby="modal-modal-title" | |||
| aria-describedby="modal-modal-description" | |||
| > | |||
| <Box | |||
| sx={{ | |||
| width: '50%', | |||
| top: '50%', | |||
| left: '50%', | |||
| position: 'absolute', | |||
| transform: 'translate(-50%, -50%)', | |||
| }} | |||
| > | |||
| {open.type === 'Shipping' && ( | |||
| <ShippingDetailsForm submitHandler={handleChangeShipping} /> | |||
| )} | |||
| {open.type === 'Contact' && ( | |||
| <ContactForm submitHandler={handleChangeContact} /> | |||
| )} | |||
| </Box> | |||
| </Modal> | |||
| ); | |||
| }; | |||
| ShippingModal.propTypes = { | |||
| open: PropType.object, | |||
| handleClose: PropType.func, | |||
| handleChangeShipping: PropType.func, | |||
| handleChangeContact: PropType.func, | |||
| }; | |||
| export default ShippingModal; | |||
| @@ -0,0 +1,21 @@ | |||
| import { loadStripe } from '@stripe/stripe-js'; | |||
| export const useStripe = async ({ lineItems }) => { | |||
| let stripePromise = null; | |||
| const getStripe = () => { | |||
| if (!stripePromise) { | |||
| stripePromise = loadStripe(process.env.NEXT_PUBLIC_API_KEY); | |||
| } | |||
| return stripePromise; | |||
| }; | |||
| const stripe = await getStripe(); | |||
| await stripe.redirectToCheckout({ | |||
| mode: 'payment', | |||
| lineItems, | |||
| successUrl: `${window.location.origin}/review`, | |||
| cancelUrl: `${window.location.origin}/cart`, | |||
| }); | |||
| }; | |||
| @@ -1,78 +1,148 @@ | |||
| const mongoose = require('mongoose'); | |||
| const validator = require('validator'); | |||
| const Product = require('./product'); | |||
| const OrderSchema = new mongoose.Schema({ | |||
| products: [Product], | |||
| time: { | |||
| type: Date, | |||
| required: [true, 'Please provide a date.'], | |||
| validate(value) { | |||
| if (!validator.isDate(value)) { | |||
| throw new Error('Not a date'); | |||
| } | |||
| const OrderSchema = new mongoose.Schema( | |||
| { | |||
| products: [ | |||
| { | |||
| category: { | |||
| type: String, | |||
| required: [true, 'Please provide a category.'], | |||
| maxlength: 100, | |||
| trim: true, | |||
| }, | |||
| name: { | |||
| type: String, | |||
| required: [true, 'Please provide a name.'], | |||
| maxlength: 100, | |||
| trim: true, | |||
| }, | |||
| image: { | |||
| type: String, | |||
| required: [true, 'Please provide an image.'], | |||
| }, | |||
| description: { | |||
| type: String, | |||
| required: [true, 'Please provide a description.'], | |||
| trim: true, | |||
| }, | |||
| place: { | |||
| type: String, | |||
| trim: true, | |||
| }, | |||
| people: { | |||
| type: String, | |||
| trim: true, | |||
| }, | |||
| process: { | |||
| type: String, | |||
| trim: true, | |||
| }, | |||
| pairing: { | |||
| type: String, | |||
| trim: true, | |||
| }, | |||
| available: { | |||
| type: Boolean, | |||
| default: true, | |||
| }, | |||
| isFeatured: { | |||
| type: Boolean, | |||
| default: false, | |||
| }, | |||
| price: { | |||
| type: Number, | |||
| required: [true, 'Please provide a price.'], | |||
| validate(value) { | |||
| if (value < 0) { | |||
| throw new Error('Price must be a postive number'); | |||
| } | |||
| }, | |||
| }, | |||
| }, | |||
| ], | |||
| time: { | |||
| type: Date, | |||
| required: [true, 'Please provide a date.'], | |||
| validate(value) { | |||
| if (!validator.isDate(value)) { | |||
| throw new Error('Not a date'); | |||
| } | |||
| }, | |||
| }, | |||
| }, | |||
| shippingAddress: { | |||
| country: { | |||
| type: String, | |||
| required: [true, 'Please provide a country.'], | |||
| trim: true, | |||
| shippingAddress: { | |||
| country: { | |||
| type: String, | |||
| required: [true, 'Please provide a country.'], | |||
| trim: true, | |||
| }, | |||
| city: { | |||
| type: String, | |||
| required: [true, 'Please provide a city.'], | |||
| trim: true, | |||
| }, | |||
| address: { | |||
| type: String, | |||
| required: [true, 'Please provide an address.'], | |||
| trim: true, | |||
| }, | |||
| address2: { | |||
| type: String, | |||
| trim: true, | |||
| }, | |||
| postcode: { | |||
| type: String, | |||
| required: [true, 'Please provide a postal code.'], | |||
| }, | |||
| email: { | |||
| type: String, | |||
| required: [true, 'Please provide an email.'], | |||
| }, | |||
| fullName: { | |||
| type: String, | |||
| required: [true, 'Please provide a name.'], | |||
| }, | |||
| }, | |||
| city: { | |||
| type: String, | |||
| required: [true, 'Please provide a city.'], | |||
| trim: true, | |||
| totalPrice: { | |||
| type: Number, | |||
| required: [true, 'Please provide a total price.'], | |||
| validate(value) { | |||
| if (value < 0) { | |||
| throw new Error('Total price must be a postive number'); | |||
| } | |||
| }, | |||
| }, | |||
| address: { | |||
| type: String, | |||
| required: [true, 'Please provide an address.'], | |||
| trim: true, | |||
| numberOfItems: { | |||
| type: Number, | |||
| required: [true, 'Please provide a total number of items.'], | |||
| validate(value) { | |||
| if (value < 0) { | |||
| throw new Error('Number of items must be a postive number'); | |||
| } | |||
| }, | |||
| }, | |||
| address2: { | |||
| type: String, | |||
| trim: true, | |||
| fulfilled: { | |||
| type: Boolean, | |||
| default: false, | |||
| }, | |||
| postcode: { | |||
| type: String, | |||
| required: [true, 'Please provide a postal code.'], | |||
| owner: { | |||
| type: mongoose.Schema.Types.ObjectId, | |||
| required: [true, 'Please provide an owner.'], | |||
| ref: 'User', | |||
| }, | |||
| }, | |||
| totalPrice: { | |||
| type: Number, | |||
| required: [true, 'Please provide a total price.'], | |||
| validate(value) { | |||
| if (value < 0) { | |||
| throw new Error('Total price must be a postive number'); | |||
| } | |||
| }, | |||
| }, | |||
| numberOfItems: { | |||
| type: Number, | |||
| required: [true, 'Please provide a total number of items.'], | |||
| validate(value) { | |||
| if (value < 0) { | |||
| throw new Error('Number of items must be a postive number'); | |||
| } | |||
| stripeCheckoutId: { | |||
| type: String, | |||
| required: [true, 'Please provide a stripe checkout id.'], | |||
| }, | |||
| }, | |||
| fulfilled: { | |||
| type: Boolean, | |||
| default: false, | |||
| }, | |||
| owner: { | |||
| type: mongoose.Schema.Types.ObjectId, | |||
| required: [true, 'Please provide an owner.'], | |||
| ref: 'User', | |||
| }, | |||
| stripeCheckoutId: { | |||
| type: String, | |||
| required: [true, 'Please provide a stripe checkout id.'], | |||
| unique: [true, 'Stripe checkout id id must be unique.'], | |||
| }, | |||
| }); | |||
| { | |||
| toJSON: { virtuals: true }, // So `res.json()` and other `JSON.stringify()` functions include virtuals | |||
| toObject: { virtuals: true }, // So `console.log()` and other functions that use `toObject()` include virtuals | |||
| } | |||
| ); | |||
| const Order = mongoose.models.Order || mongoose.model('Order', OrderSchema); | |||
| const Order = | |||
| mongoose.models.Order || mongoose.model('Order', OrderSchema, 'Order'); | |||
| module.exports = Order; | |||
| @@ -6,67 +6,73 @@ import { | |||
| const mongoose = require('mongoose'); | |||
| const validator = require('validator'); | |||
| const UserSchema = new mongoose.Schema({ | |||
| fullName: { | |||
| type: String, | |||
| required: [true, 'Please provide a name.'], | |||
| maxlength: [60, 'Name cannot be more than 60 characters'], | |||
| trim: true, | |||
| }, | |||
| username: { | |||
| type: String, | |||
| required: [true, 'Please provide an username.'], | |||
| unique: [true, 'Username must be unique.'], | |||
| maxlength: [60, 'Name cannot be more than 60 characters'], | |||
| trim: true, | |||
| }, | |||
| email: { | |||
| type: String, | |||
| unique: [true, 'Email must be unique.'], | |||
| required: [true, 'Please provide an email.'], | |||
| trim: true, | |||
| lowercase: true, | |||
| validate(value) { | |||
| if (!validator.isEmail(value)) { | |||
| throw new Error('Email is invalid'); | |||
| } | |||
| const UserSchema = new mongoose.Schema( | |||
| { | |||
| fullName: { | |||
| type: String, | |||
| required: [true, 'Please provide a name.'], | |||
| maxlength: [60, 'Name cannot be more than 60 characters'], | |||
| trim: true, | |||
| }, | |||
| }, | |||
| password: { | |||
| type: String, | |||
| required: [true, 'Please provide a password.'], | |||
| minlength: 7, | |||
| trim: true, | |||
| validate(value) { | |||
| if (value.toLowerCase().includes('password')) { | |||
| throw new Error('Password cannot contain "password"'); | |||
| } | |||
| username: { | |||
| type: String, | |||
| required: [true, 'Please provide an username.'], | |||
| unique: [true, 'Username must be unique.'], | |||
| maxlength: [60, 'Name cannot be more than 60 characters'], | |||
| trim: true, | |||
| }, | |||
| email: { | |||
| type: String, | |||
| unique: [true, 'Email must be unique.'], | |||
| required: [true, 'Please provide an email.'], | |||
| trim: true, | |||
| lowercase: true, | |||
| validate(value) { | |||
| if (!validator.isEmail(value)) { | |||
| throw new Error('Email is invalid'); | |||
| } | |||
| }, | |||
| }, | |||
| password: { | |||
| type: String, | |||
| required: [true, 'Please provide a password.'], | |||
| minlength: 7, | |||
| trim: true, | |||
| validate(value) { | |||
| if (value.toLowerCase().includes('password')) { | |||
| throw new Error('Password cannot contain "password"'); | |||
| } | |||
| }, | |||
| }, | |||
| country: { | |||
| type: String, | |||
| required: [true, 'Please provide a country.'], | |||
| trim: true, | |||
| }, | |||
| city: { | |||
| type: String, | |||
| required: [true, 'Please provide a city.'], | |||
| trim: true, | |||
| }, | |||
| address: { | |||
| type: String, | |||
| required: [true, 'Please provide an address.'], | |||
| trim: true, | |||
| }, | |||
| address2: { | |||
| type: String, | |||
| trim: true, | |||
| }, | |||
| postcode: { | |||
| type: String, | |||
| required: [true, 'Please provide a postal code.'], | |||
| }, | |||
| }, | |||
| country: { | |||
| type: String, | |||
| required: [true, 'Please provide a country.'], | |||
| trim: true, | |||
| }, | |||
| city: { | |||
| type: String, | |||
| required: [true, 'Please provide a city.'], | |||
| trim: true, | |||
| }, | |||
| address: { | |||
| type: String, | |||
| required: [true, 'Please provide an address.'], | |||
| trim: true, | |||
| }, | |||
| address2: { | |||
| type: String, | |||
| trim: true, | |||
| }, | |||
| postcode: { | |||
| type: String, | |||
| required: [true, 'Please provide a postal code.'], | |||
| }, | |||
| }); | |||
| { | |||
| toJSON: { virtuals: true }, // So `res.json()` and other `JSON.stringify()` functions include virtuals | |||
| toObject: { virtuals: true }, // So `console.log()` and other functions that use `toObject()` include virtuals | |||
| } | |||
| ); | |||
| UserSchema.virtual('orders', { | |||
| ref: 'Order', | |||
| @@ -95,6 +101,7 @@ UserSchema.statics.findByCredentials = async (username, password) => { | |||
| city: user.city, | |||
| country: user.country, | |||
| postcode: user.postcode, | |||
| orders: user.orders, | |||
| _id: user._id, | |||
| }; | |||
| return userData; | |||
| @@ -110,5 +117,5 @@ UserSchema.pre('save', async function (next) { | |||
| next(); | |||
| }); | |||
| const User = mongoose.models.User || mongoose.model('User', UserSchema); | |||
| const User = mongoose.models.User || mongoose.model('User', UserSchema, 'User'); | |||
| module.exports = User; | |||
| @@ -5,6 +5,11 @@ const nextConfig = { | |||
| images: { | |||
| domains: ['www.business2community.com'], | |||
| }, | |||
| env: { | |||
| NEXT_PUBLIC_STRIPE_PUBLIC_API_KEY: | |||
| process.env.NEXT_PUBLIC_STRIPE_PUBLIC_API_KEY, | |||
| NEXT_PUBLIC_MAP_KEY: process.env.NEXT_PUBLIC_NEXT_PUBLIC_MAP_KEY, | |||
| }, | |||
| reactStrictMode: true, | |||
| swcMinify: true, | |||
| i18n, | |||
| @@ -18,6 +18,8 @@ | |||
| "@mui/codemod": "^5.8.7", | |||
| "@mui/icons-material": "^5.8.4", | |||
| "@mui/material": "^5.9.2", | |||
| "@react-google-maps/api": "^2.12.2", | |||
| "@stripe/stripe-js": "^1.35.0", | |||
| "@tanstack/react-query": "^4.0.10", | |||
| "bcryptjs": "^2.4.3", | |||
| "date-fns": "^2.29.1", | |||
| @@ -27,10 +29,12 @@ | |||
| "next": "12.2.3", | |||
| "next-auth": "^4.10.2", | |||
| "next-i18next": "^11.3.0", | |||
| "nookies": "^2.5.2", | |||
| "prop-types": "^15.8.1", | |||
| "react": "18.2.0", | |||
| "react-dom": "18.2.0", | |||
| "sass": "^1.54.0", | |||
| "stripe": "^10.8.0", | |||
| "swr": "^1.3.0", | |||
| "validator": "^13.7.0", | |||
| "yup": "^0.32.11" | |||
| @@ -1,9 +1,10 @@ | |||
| const Order = require('../../../models/order'); | |||
| import dbConnect from '../../../utils/helpers/dbHelpers'; | |||
| const mongoose = require('mongoose'); | |||
| async function handler(req, res) { | |||
| const { method } = req; | |||
| const ownerID = req.query.ownerID; | |||
| await dbConnect(); | |||
| switch (method) { | |||
| @@ -16,7 +17,29 @@ async function handler(req, res) { | |||
| .status(201) | |||
| .json({ message: 'Your order was submitted successfully!', order }); | |||
| } catch (error) { | |||
| res.status(400).json({ success: false }); | |||
| res.status(400).json({ message: error }); | |||
| } | |||
| break; | |||
| } | |||
| case 'GET': { | |||
| try { | |||
| const objectId = mongoose.Types.ObjectId(ownerID); | |||
| const orders = await Order.find({ owner: objectId }); | |||
| if (!orders) { | |||
| res.status(200).json({ | |||
| message: | |||
| 'There are currently no orders in our database for the selected owner.', | |||
| orders: [], | |||
| }); | |||
| } | |||
| res.status(200).json({ | |||
| message: | |||
| 'All orders from our database for the selected owner were fetched successfully.', | |||
| orders, | |||
| }); | |||
| } catch (error) { | |||
| res.status(400).json({ message: error }); | |||
| } | |||
| break; | |||
| } | |||
| @@ -9,7 +9,7 @@ async function handler(req, res) { | |||
| switch (method) { | |||
| case 'GET': { | |||
| try { | |||
| const featuredProducts = await Product.find({ isFeatured: false }); | |||
| const featuredProducts = await Product.find({ isFeatured: true }); | |||
| if (!featuredProducts) { | |||
| res.status(200).json({ | |||
| @@ -0,0 +1,46 @@ | |||
| const User = require('../../../models/user'); | |||
| import dbConnect from '../../../utils/helpers/dbHelpers'; | |||
| async function handler(req, res) { | |||
| const { method } = req; | |||
| await dbConnect(); | |||
| switch (method) { | |||
| case 'PATCH': { | |||
| console.log(req.body); | |||
| const updates = Object.keys(req.body.userData); | |||
| const allowedUpdates = [ | |||
| 'fullName', | |||
| 'email', | |||
| 'address', | |||
| 'address2', | |||
| 'city', | |||
| 'country', | |||
| 'postcode', | |||
| ]; | |||
| const isValidOperation = updates.every((update) => | |||
| allowedUpdates.includes(update) | |||
| ); | |||
| if (!isValidOperation) { | |||
| return res.status(400).send({ error: 'Invalid updates!' }); | |||
| } | |||
| try { | |||
| const user = await User.findOne({ _id: req.body._id }); | |||
| updates.forEach((update) => (user[update] = req.body.userData[update])); | |||
| await user.save(); | |||
| res.send({ | |||
| user, | |||
| message: 'User profile updated successfully.', | |||
| }); | |||
| } catch (error) { | |||
| res.status(400).json({ message: error.message }); | |||
| } | |||
| break; | |||
| } | |||
| } | |||
| } | |||
| export default handler; | |||
| @@ -1,7 +1,25 @@ | |||
| import nookies from 'nookies'; | |||
| import CheckoutContent from '../../components/checkout-content/CheckoutContent'; | |||
| const CheckoutPage = () => { | |||
| return <CheckoutContent></CheckoutContent>; | |||
| }; | |||
| export const getServerSideProps = async (ctx) => { | |||
| const cookies = nookies.get(ctx); | |||
| if (!cookies['checkout-session']) { | |||
| return { | |||
| redirect: { | |||
| destination: '/cart', | |||
| permanent: false, | |||
| }, | |||
| }; | |||
| } | |||
| return { | |||
| props: {}, | |||
| }; | |||
| }; | |||
| export default CheckoutPage; | |||
| @@ -1,24 +1,10 @@ | |||
| import { Button } from '@mui/material'; | |||
| import { getSession, signOut, useSession } from 'next-auth/react'; | |||
| import { getSession } from 'next-auth/react'; | |||
| import ProfileContent from '../../components/profile-content/ProfileContent'; | |||
| import { LOGIN_PAGE } from '../../constants/pages'; | |||
| import { getOrdersForOwner } from '../../requests/orders/getOrdersForOwnerRequest'; | |||
| const ProfilePage = () => { | |||
| const { data: session } = useSession(); | |||
| console.log(session); | |||
| function logoutHandler() { | |||
| signOut(); | |||
| } | |||
| return ( | |||
| <> | |||
| <ProfileContent></ProfileContent> | |||
| <Button color="inherit" onClick={logoutHandler}> | |||
| Logout | |||
| </Button> | |||
| </> | |||
| ); | |||
| const ProfilePage = (props) => { | |||
| return <ProfileContent orders={props.orders.orders}></ProfileContent>; | |||
| }; | |||
| export async function getServerSideProps(context) { | |||
| @@ -33,8 +19,10 @@ export async function getServerSideProps(context) { | |||
| }; | |||
| } | |||
| const orders = await getOrdersForOwner(session.user._id); | |||
| return { | |||
| props: { session }, | |||
| props: { orders }, | |||
| }; | |||
| } | |||
| @@ -1,7 +1,25 @@ | |||
| import nookies from 'nookies'; | |||
| import ReviewContent from '../../components/review-content/ReviewContent'; | |||
| const ReviewPage = () => { | |||
| return <ReviewContent></ReviewContent>; | |||
| }; | |||
| export const getServerSideProps = async (ctx) => { | |||
| const cookies = nookies.get(ctx); | |||
| if (!cookies['review-session']) { | |||
| return { | |||
| redirect: { | |||
| destination: '/cart', | |||
| permanent: false, | |||
| }, | |||
| }; | |||
| } | |||
| return { | |||
| props: {}, | |||
| }; | |||
| }; | |||
| export default ReviewPage; | |||
| @@ -1,7 +1,24 @@ | |||
| import nookies from 'nookies'; | |||
| import ShippingContent from '../../components/shipping-content/ShippingContent'; | |||
| const ShippingPage = () => { | |||
| return <ShippingContent></ShippingContent>; | |||
| }; | |||
| export const getServerSideProps = async (ctx) => { | |||
| const cookies = nookies.get(ctx); | |||
| if (!cookies['shipping-session']) { | |||
| return { | |||
| redirect: { | |||
| destination: '/cart', | |||
| permanent: false, | |||
| }, | |||
| }; | |||
| } | |||
| return { | |||
| props: {}, | |||
| }; | |||
| }; | |||
| export default ShippingPage; | |||
| @@ -0,0 +1,57 @@ | |||
| <?xml version="1.0" encoding="iso-8859-1"?> | |||
| <!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> | |||
| <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" | |||
| viewBox="0 0 384.971 384.971" style="enable-background:new 0 0 384.971 384.971;" xml:space="preserve"> | |||
| <g> | |||
| <g id="Sign_Out"> | |||
| <path d="M180.455,360.91H24.061V24.061h156.394c6.641,0,12.03-5.39,12.03-12.03s-5.39-12.03-12.03-12.03H12.03 | |||
| C5.39,0.001,0,5.39,0,12.031V372.94c0,6.641,5.39,12.03,12.03,12.03h168.424c6.641,0,12.03-5.39,12.03-12.03 | |||
| C192.485,366.299,187.095,360.91,180.455,360.91z"/> | |||
| <path d="M381.481,184.088l-83.009-84.2c-4.704-4.752-12.319-4.74-17.011,0c-4.704,4.74-4.704,12.439,0,17.179l62.558,63.46H96.279 | |||
| c-6.641,0-12.03,5.438-12.03,12.151c0,6.713,5.39,12.151,12.03,12.151h247.74l-62.558,63.46c-4.704,4.752-4.704,12.439,0,17.179 | |||
| c4.704,4.752,12.319,4.752,17.011,0l82.997-84.2C386.113,196.588,386.161,188.756,381.481,184.088z"/> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| <g> | |||
| </g> | |||
| </svg> | |||
| @@ -7,4 +7,5 @@ export default { | |||
| productsByCategory: '/api/product/category/', | |||
| featuredProducts: '/api/product/featured-products', | |||
| order: '/api/order', | |||
| userUpdate: '/api/user', | |||
| }; | |||
| @@ -0,0 +1,15 @@ | |||
| import apiEndpoints from '../apiEndpoints'; | |||
| export const getOrdersForOwner = async (id) => { | |||
| const response = await fetch( | |||
| `http://localhost:3000${apiEndpoints.order}?ownerID=${id}` | |||
| ); | |||
| const data = await response.json(); | |||
| if (!response.ok) { | |||
| throw new Error(data.message || 'Something went wrong!'); | |||
| } | |||
| return data; | |||
| }; | |||
| @@ -0,0 +1,20 @@ | |||
| import apiEndpoints from '../apiEndpoints'; | |||
| export const updateUser = async (userData, _id) => { | |||
| console.log(userData, _id); | |||
| const response = await fetch(apiEndpoints.userUpdate, { | |||
| method: 'PATCH', | |||
| body: JSON.stringify({ userData, _id }), | |||
| headers: { | |||
| 'Content-Type': 'application/json', | |||
| }, | |||
| }); | |||
| const data = await response.json(); | |||
| if (!response.ok) { | |||
| throw new Error(data.message || 'Something went wrong!'); | |||
| } | |||
| return data; | |||
| }; | |||
| @@ -1,8 +1,5 @@ | |||
| import * as Yup from 'yup'; | |||
| export const contactSchema = Yup.object().shape({ | |||
| firstName: Yup.string().required('First name is required'), | |||
| lastName: Yup.string().required('Last name is required'), | |||
| email: Yup.string().email('Enter valid email').required('Email is required'), | |||
| message: Yup.string().required('Message is required'), | |||
| }); | |||
| @@ -101,6 +101,8 @@ const useStorage = () => { | |||
| const clearCart = () => { | |||
| setStorage(CART_KEY, []); | |||
| setTotalQuantity(0); | |||
| setTotalPrice(0); | |||
| setCartStorage([]); | |||
| }; | |||
| @@ -6,7 +6,10 @@ const CheckoutContext = createContext({ | |||
| }); | |||
| const CheckoutDispatchContext = createContext({ | |||
| addCheckoutValue: (products, userInfo, userID) => {}, | |||
| changeContact: (email) => {}, | |||
| changeShippingData: (shippingData) => {}, | |||
| clearCheckout: () => {}, | |||
| parseCheckoutValue: () => {}, | |||
| }); | |||
| export const useCheckoutData = () => { | |||
| @@ -23,12 +26,9 @@ const useCheckout = () => { | |||
| ); | |||
| const addCheckoutValue = (products, userInfo, userID) => { | |||
| console.log('hello from context'); | |||
| const items = getSStorage(CHECKOUT_KEY); | |||
| setSStorage(CHECKOUT_KEY, { products, userInfo, userID }); | |||
| setCheckoutStorage(items); | |||
| setCheckoutStorage({ products, userInfo, userID }); | |||
| }; | |||
| const clearCheckout = () => { | |||
| @@ -36,9 +36,53 @@ const useCheckout = () => { | |||
| setCheckoutStorage({}); | |||
| }; | |||
| const parseCheckoutValue = () => { | |||
| const items = checkoutStorage; | |||
| const date = new Date(); | |||
| const dataToStore = { | |||
| products: items?.products?.map((el) => el.product), | |||
| time: date.toLocaleDateString(), | |||
| shippingAddress: items?.userInfo, | |||
| totalPrice: items?.products | |||
| ?.map((entry) => entry?.product.price * entry?.quantity) | |||
| ?.reduce((accum, curValue) => accum + curValue), | |||
| numberOfItems: items?.products | |||
| ?.map((entry) => entry?.quantity) | |||
| ?.reduce((accum, curValue) => accum + curValue), | |||
| fulfilled: false, | |||
| owner: items?.userID, | |||
| stripeCheckoutId: `Stripe test4`, | |||
| }; | |||
| return dataToStore; | |||
| }; | |||
| const changeContact = (email) => { | |||
| const items = getSStorage(CHECKOUT_KEY); | |||
| items.userInfo.email = email; | |||
| setSStorage(CHECKOUT_KEY, { ...items }); | |||
| setCheckoutStorage(items); | |||
| }; | |||
| const changeShippingData = (shippingData) => { | |||
| const items = getSStorage(CHECKOUT_KEY); | |||
| items.userInfo = { email: items.userInfo.email, ...shippingData }; | |||
| setSStorage(CHECKOUT_KEY, { ...items }); | |||
| setCheckoutStorage(items); | |||
| }; | |||
| return { | |||
| addCheckoutValue, | |||
| clearCheckout, | |||
| parseCheckoutValue, | |||
| changeContact, | |||
| changeShippingData, | |||
| setCheckoutStorage, | |||
| checkoutStorage, | |||
| }; | |||
| @@ -50,6 +94,9 @@ const CheckoutProvider = ({ children }) => { | |||
| setCheckoutStorage, | |||
| addCheckoutValue, | |||
| clearCheckout, | |||
| parseCheckoutValue, | |||
| changeContact, | |||
| changeShippingData, | |||
| } = useCheckout(); | |||
| return ( | |||
| @@ -59,6 +106,9 @@ const CheckoutProvider = ({ children }) => { | |||
| setCheckoutStorage, | |||
| addCheckoutValue, | |||
| clearCheckout, | |||
| parseCheckoutValue, | |||
| changeContact, | |||
| changeShippingData, | |||
| }} | |||
| > | |||
| {children} | |||
| @@ -0,0 +1,23 @@ | |||
| import { loadStripe } from '@stripe/stripe-js'; | |||
| export async function stripe({ lineItems }) { | |||
| let stripePromise = null; | |||
| const getStripe = () => { | |||
| if (!stripePromise) { | |||
| stripePromise = loadStripe( | |||
| 'pk_test_51Lg3phDY7dvAcw2fNi1ACbS7S0SrEQs7SQUwA9YfKrLvjRH1jyV4nwM8fg32Adfxzn5uXitNGqsyPPtavpdR8UU800rxDPajp8' | |||
| ); | |||
| } | |||
| return stripePromise; | |||
| }; | |||
| const stripe = await getStripe(); | |||
| await stripe.redirectToCheckout({ | |||
| mode: 'payment', | |||
| lineItems, | |||
| successUrl: `${window.location.origin}/review`, | |||
| cancelUrl: `${window.location.origin}/cart`, | |||
| }); | |||
| } | |||