| <Paper | <Paper | ||||
| sx={{ | sx={{ | ||||
| p: 1, | p: 1, | ||||
| width: '88%', | |||||
| width: { lg: '88%', xs: '73%', md: '80%' }, | |||||
| mb: 2, | mb: 2, | ||||
| ml: 12, | ml: 12, | ||||
| backgroundColor: '#f2f2f2', | backgroundColor: '#f2f2f2', | ||||
| display: 'flex', | display: 'flex', | ||||
| justifyContent: { xs: 'center', md: 'none' }, | |||||
| }} | }} | ||||
| elevation={3} | elevation={3} | ||||
| > | > | ||||
| <Box sx={{ width: '30%' }}> | |||||
| <Box sx={{ width: '30%', display: { xs: 'none', md: 'block' } }}> | |||||
| <Image | <Image | ||||
| src="/images/coffee-mug.svg" | src="/images/coffee-mug.svg" | ||||
| alt="profile" | alt="profile" | ||||
| textAlign: 'center', | textAlign: 'center', | ||||
| height: 25, | height: 25, | ||||
| fontWeight: 600, | fontWeight: 600, | ||||
| fontSize: 20, | |||||
| ml: { xs: -1, sm: 0 }, | |||||
| fontSize: { xs: 16, sm: 20 }, | |||||
| }} | }} | ||||
| > | > | ||||
| {product?.name} | {product?.name} | ||||
| width: '20%', | width: '20%', | ||||
| justifyContent: 'center', | justifyContent: 'center', | ||||
| alignItems: 'center', | alignItems: 'center', | ||||
| ml: { xs: 2, sm: 0 }, | |||||
| }} | }} | ||||
| > | > | ||||
| <Typography | <Typography | ||||
| </Box> | </Box> | ||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| ml: 3, | |||||
| ml: { xs: 5, sm: 3 }, | |||||
| display: 'flex', | display: 'flex', | ||||
| flexDirection: 'column', | flexDirection: 'column', | ||||
| width: '20%', | width: '20%', | ||||
| width: '100%', | width: '100%', | ||||
| textAlign: 'center', | textAlign: 'center', | ||||
| height: 25, | height: 25, | ||||
| fontSize: 18, | |||||
| fontSize: { xs: 15, md: 18 }, | |||||
| }} | }} | ||||
| > | > | ||||
| Price: ${product?.price} | Price: ${product?.price} |
| import { Box, Paper, Typography } from '@mui/material'; | import { Box, Paper, Typography } from '@mui/material'; | ||||
| import Image from 'next/image'; | import Image from 'next/image'; | ||||
| import PropType from 'prop-types'; | |||||
| const DataCard = () => { | |||||
| const DataCard = ({ data, quantity }) => { | |||||
| return ( | return ( | ||||
| <Paper | <Paper | ||||
| sx={{ | sx={{ | ||||
| p: 3, | p: 3, | ||||
| width: '100%', | |||||
| width: { lg: '100%', xs: '35%' }, | |||||
| mb: 2, | mb: 2, | ||||
| ml: { lg: 0, xs: 6 }, | |||||
| backgroundColor: '#f2f2f2', | backgroundColor: '#f2f2f2', | ||||
| display: 'flex', | display: 'flex', | ||||
| flex: { xs: [0, 0, '32%'], lg: 'none' }, | |||||
| }} | }} | ||||
| elevation={3} | elevation={3} | ||||
| > | > | ||||
| <Box sx={{ width: '30%', borderRadius: 4, overflow: 'hidden' }}> | |||||
| <Box | |||||
| sx={{ | |||||
| width: '30%', | |||||
| borderRadius: 4, | |||||
| overflow: 'hidden', | |||||
| display: { xs: 'none', lg: 'block' }, | |||||
| }} | |||||
| > | |||||
| <Image | <Image | ||||
| src="/images/coffee-mug.svg" | src="/images/coffee-mug.svg" | ||||
| alt="profile" | alt="profile" | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| <Box | <Box | ||||
| sx={{ ml: 3, display: 'flex', flexDirection: 'column', width: '60%' }} | |||||
| sx={{ | |||||
| ml: 3, | |||||
| display: 'flex', | |||||
| flexDirection: 'column', | |||||
| width: { lg: '60%', sx: '100%' }, | |||||
| }} | |||||
| > | > | ||||
| <Typography | <Typography | ||||
| sx={{ | sx={{ | ||||
| textAlign: 'center', | textAlign: 'center', | ||||
| height: 25, | height: 25, | ||||
| fontWeight: 600, | fontWeight: 600, | ||||
| fontSize: 20, | |||||
| fontSize: { md: 20, xs: 16 }, | |||||
| }} | |||||
| > | |||||
| {data.name} - x{quantity} | |||||
| </Typography> | |||||
| <Typography | |||||
| sx={{ | |||||
| mt: { sm: 3, xs: 6 }, | |||||
| fontSize: 14, | |||||
| }} | }} | ||||
| > | > | ||||
| Begin Mug in White | |||||
| {data.description} | |||||
| </Typography> | </Typography> | ||||
| <Typography | <Typography | ||||
| sx={{ | sx={{ | ||||
| mt: 3, | |||||
| mt: { lg: 3, xs: 1 }, | |||||
| textAlign: 'right', | |||||
| fontSize: 14, | fontSize: 14, | ||||
| }} | }} | ||||
| > | > | ||||
| Simple and beautiful Begin mug. Perfect companion for your next | |||||
| delicious cup of coffee. | |||||
| ${data.price} (per unit) | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| </Paper> | </Paper> | ||||
| ); | ); | ||||
| }; | }; | ||||
| DataCard.propTypes = { | |||||
| product: PropType.shape({ | |||||
| category: PropType.string, | |||||
| name: PropType.string, | |||||
| image: PropType.string, | |||||
| description: PropType.string, | |||||
| place: PropType.string, | |||||
| people: PropType.string, | |||||
| process: PropType.string, | |||||
| pairing: PropType.string, | |||||
| available: PropType.Boolean, | |||||
| isFeatured: PropType.Boolean, | |||||
| price: PropType.number, | |||||
| customID: PropType.string, | |||||
| }), | |||||
| quantity: PropType.number, | |||||
| }; | |||||
| export default DataCard; | export default DataCard; |
| elevation={3} | elevation={3} | ||||
| > | > | ||||
| <Typography sx={{ fontWeight: 600 }}> | <Typography sx={{ fontWeight: 600 }}> | ||||
| Order placed on {data.date} | |||||
| Order placed on: {data.date} | |||||
| </Typography> | </Typography> | ||||
| <Divider /> | <Divider /> | ||||
| <Typography sx={{ mt: 1 }}>By: {data.name}</Typography> | <Typography sx={{ mt: 1 }}>By: {data.name}</Typography> |
| import { Button, Divider, Paper, Typography } from '@mui/material'; | import { Button, Divider, Paper, Typography } from '@mui/material'; | ||||
| import { Box } from '@mui/system'; | import { Box } from '@mui/system'; | ||||
| import Image from 'next/image'; | import Image from 'next/image'; | ||||
| import { useRouter } from 'next/router'; | |||||
| import { setCookie } from 'nookies'; | |||||
| import PropType from 'prop-types'; | import PropType from 'prop-types'; | ||||
| const OrderSummaryCard = ({ data }) => { | const OrderSummaryCard = ({ data }) => { | ||||
| const router = useRouter(); | |||||
| return ( | return ( | ||||
| <Paper | <Paper | ||||
| sx={{ p: 3, width: '100%', mb: 2, backgroundColor: '#f1f1f1' }} | sx={{ p: 3, width: '100%', mb: 2, backgroundColor: '#f1f1f1' }} | ||||
| startIcon={ | startIcon={ | ||||
| <Image src="/images/lock.svg" alt="lock" width={18} height={18} /> | <Image src="/images/lock.svg" alt="lock" width={18} height={18} /> | ||||
| } | } | ||||
| 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 | Proceed to Checkout | ||||
| </Button> | </Button> | ||||
| OrderSummaryCard.propTypes = { | OrderSummaryCard.propTypes = { | ||||
| data: PropType.shape({ | data: PropType.shape({ | ||||
| totalPrice: PropType.number, | totalPrice: PropType.number, | ||||
| totalQuantity: PropType.number, | |||||
| }), | }), | ||||
| }; | }; | ||||
| import { Breadcrumbs, Divider, Grid, Typography } from '@mui/material'; | |||||
| import { Grid, Typography } from '@mui/material'; | |||||
| import { Box } from '@mui/system'; | import { Box } from '@mui/system'; | ||||
| import { destroyCookie } from 'nookies'; | |||||
| import { useEffect } from 'react'; | |||||
| import { useStore, useStoreUpdate } from '../../store/cart-context'; | import { useStore, useStoreUpdate } from '../../store/cart-context'; | ||||
| import CartCard from '../cards/cart-card/CartCard'; | import CartCard from '../cards/cart-card/CartCard'; | ||||
| import OrderSummaryCard from '../cards/order-summary-card/OrderSummaryCard'; | import OrderSummaryCard from '../cards/order-summary-card/OrderSummaryCard'; | ||||
| import StepTitle from '../layout/steps-title/StepTitle'; | |||||
| const CartContent = () => { | const CartContent = () => { | ||||
| const { cartStorage, totalPrice } = useStore(); | |||||
| const { cartStorage, totalPrice, totalQuantity } = useStore(); | |||||
| const { removeCartValue, updateItemQuantity } = useStoreUpdate(); | const { removeCartValue, updateItemQuantity } = useStoreUpdate(); | ||||
| useEffect(() => { | |||||
| destroyCookie(null, 'checkout-session', { | |||||
| path: '/', | |||||
| }); | |||||
| }, []); | |||||
| const mapProductsToDom = () => { | const mapProductsToDom = () => { | ||||
| if (cartStorage?.length) { | if (cartStorage?.length) { | ||||
| return cartStorage.map((element, i) => ( | return cartStorage.map((element, i) => ( | ||||
| ); | ); | ||||
| } | } | ||||
| }; | }; | ||||
| return ( | return ( | ||||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | <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()} | {mapProductsToDom()} | ||||
| </Grid> | </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 | <OrderSummaryCard | ||||
| data={{ totalPrice: totalPrice }} | |||||
| data={{ totalPrice: totalPrice, totalQuantity: totalQuantity }} | |||||
| ></OrderSummaryCard> | ></OrderSummaryCard> | ||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> |
| import { Breadcrumbs, Divider, Grid, Typography } from '@mui/material'; | |||||
| import { Grid, Typography } from '@mui/material'; | |||||
| import { Box } from '@mui/system'; | 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 DataCard from '../cards/data-card/DataCard'; | ||||
| import ShippingDetailsForm from '../forms/shipping-details/ShippingDetailsForm'; | import ShippingDetailsForm from '../forms/shipping-details/ShippingDetailsForm'; | ||||
| import StepTitle from '../layout/steps-title/StepTitle'; | |||||
| const CheckoutContent = () => { | const CheckoutContent = () => { | ||||
| const { cartStorage } = useStore(); | |||||
| const { addCheckoutValue } = useCheckoutDataUpdate(); | |||||
| const { data: session } = useSession(); | |||||
| const router = useRouter(); | |||||
| const submitHandler = (formValues) => { | |||||
| addCheckoutValue( | |||||
| cartStorage, | |||||
| { ...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 ( | return ( | ||||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | <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 }}> | <Grid item xs={12} sx={{ mt: 1 }}> | ||||
| <Typography sx={{ pl: 12, fontSize: 20 }}> | <Typography sx={{ pl: 12, fontSize: 20 }}> | ||||
| The following fields will be used as the shipping details for your | The following fields will be used as the shipping details for your | ||||
| order | order | ||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={8}> | |||||
| <ShippingDetailsForm backBtn={true}></ShippingDetailsForm> | |||||
| <Grid item lg={8} xs={12}> | |||||
| <ShippingDetailsForm | |||||
| backBtn={true} | |||||
| isCheckout={true} | |||||
| submitHandler={submitHandler} | |||||
| ></ShippingDetailsForm> | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={4}> | |||||
| <Box sx={{ width: '80%', mt: 2 }}> | |||||
| <DataCard></DataCard> | |||||
| <DataCard></DataCard> | |||||
| <DataCard></DataCard> | |||||
| <Grid item lg={4} xs={12}> | |||||
| <Box | |||||
| sx={{ | |||||
| width: '80%', | |||||
| mt: 2, | |||||
| height: '100%', | |||||
| ml: { xs: 12, lg: 0 }, | |||||
| display: { lg: 'block', xs: 'flex' }, | |||||
| flexWrap: { xs: 'wrap', lg: 'none' }, | |||||
| justifyContent: { xs: 'center', lg: 'none' }, | |||||
| }} | |||||
| > | |||||
| {mapProductsToDom()} | |||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> |
| import { | |||||
| Box, | |||||
| Button, | |||||
| Container, | |||||
| Grid, | |||||
| TextField, | |||||
| Typography | |||||
| } from '@mui/material'; | |||||
| import { Box, Button, Paper, TextField } from '@mui/material'; | |||||
| import { useFormik } from 'formik'; | 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'; | 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 | <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" | name="email" | ||||
| label={t('forms:Email')} | |||||
| label="Email" | |||||
| margin="normal" | margin="normal" | ||||
| value={formik.values.email} | value={formik.values.email} | ||||
| onChange={formik.handleChange} | onChange={formik.handleChange} | ||||
| error={formik.touched.email && Boolean(formik.errors.email)} | error={formik.touched.email && Boolean(formik.errors.email)} | ||||
| helperText={formik.touched.email && formik.errors.email} | helperText={formik.touched.email && formik.errors.email} | ||||
| autoFocus | |||||
| fullWidth | 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> | </Box> | ||||
| </Container> | |||||
| ); | |||||
| }; | |||||
| export default ContactForm; | |||||
| </Box> | |||||
| </Paper> | |||||
| ); | |||||
| }; | |||||
| ContactForm.propTypes = { | |||||
| submitHandler: PropType.func, | |||||
| }; | |||||
| export default ContactForm; |
| import { Box, Button, Paper, TextField } from '@mui/material'; | import { Box, Button, Paper, TextField } from '@mui/material'; | ||||
| import { useFormik } from 'formik'; | import { useFormik } from 'formik'; | ||||
| import { useSession } from 'next-auth/react'; | import { useSession } from 'next-auth/react'; | ||||
| import { useRouter } from 'next/router'; | |||||
| import PropType from 'prop-types'; | import PropType from 'prop-types'; | ||||
| import { useState } from 'react'; | import { useState } from 'react'; | ||||
| import { shippingDetailsSchema } from '../../../schemas/shippingDetailsSchema'; | import { shippingDetailsSchema } from '../../../schemas/shippingDetailsSchema'; | ||||
| import ErrorMessageComponent from '../../mui/ErrorMessageComponent'; | import ErrorMessageComponent from '../../mui/ErrorMessageComponent'; | ||||
| const ShippingDetailsForm = ({ backBtn = false }) => { | |||||
| const ShippingDetailsForm = ({ | |||||
| backBtn = false, | |||||
| isCheckout = false, | |||||
| submitHandler, | |||||
| enableBtn = true, | |||||
| }) => { | |||||
| const [error] = useState({ hasError: false, errorMessage: '' }); | const [error] = useState({ hasError: false, errorMessage: '' }); | ||||
| const { data: session } = useSession(); | const { data: session } = useSession(); | ||||
| const submitHandler = async (values) => { | |||||
| console.log(values); | |||||
| const router = useRouter(); | |||||
| const formikSubmitHandler = async (values) => { | |||||
| submitHandler(values); | |||||
| }; | }; | ||||
| const formik = useFormik({ | const formik = useFormik({ | ||||
| initialValues: { | initialValues: { | ||||
| fullName: session.user.fullName, | |||||
| address: session.user.address, | |||||
| address2: session.user.address2, | |||||
| city: session.user.city, | |||||
| country: session.user.country, | |||||
| postcode: 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, | validationSchema: shippingDetailsSchema, | ||||
| onSubmit: submitHandler, | |||||
| onSubmit: formikSubmitHandler, | |||||
| validateOnBlur: true, | validateOnBlur: true, | ||||
| enableReinitialize: true, | enableReinitialize: true, | ||||
| }); | }); | ||||
| return ( | return ( | ||||
| <Paper | <Paper | ||||
| sx={{ p: 3, width: '90%', ml: 12, backgroundColor: '#f2f2f2' }} | |||||
| sx={{ p: 3, width: '90%', ml: 12, mt: 2, backgroundColor: '#f2f2f2' }} | |||||
| elevation={3} | elevation={3} | ||||
| > | > | ||||
| <Box | <Box | ||||
| color: 'white', | color: 'white', | ||||
| mr: 2, | mr: 2, | ||||
| }} | }} | ||||
| onClick={() => { | |||||
| router.push('/cart'); | |||||
| }} | |||||
| > | > | ||||
| Back to cart | Back to cart | ||||
| </Button> | </Button> | ||||
| mb: 2, | mb: 2, | ||||
| backgroundColor: '#CBA213', | backgroundColor: '#CBA213', | ||||
| height: 50, | height: 50, | ||||
| width: 150, | |||||
| width: isCheckout ? 200 : 150, | |||||
| textTransform: 'none', | textTransform: 'none', | ||||
| color: 'white', | color: 'white', | ||||
| }} | }} | ||||
| disabled={!enableBtn} | |||||
| onClick={() => { | |||||
| submitHandler; | |||||
| }} | |||||
| > | > | ||||
| Submit Details | |||||
| {isCheckout ? 'Proceed to shipping' : 'Submit Details'} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| ShippingDetailsForm.propTypes = { | ShippingDetailsForm.propTypes = { | ||||
| backBtn: PropType.Boolean, | backBtn: PropType.Boolean, | ||||
| isCheckout: PropType.Boolean, | |||||
| submitHandler: PropType.func, | |||||
| }; | }; | ||||
| export default ShippingDetailsForm; | export default ShippingDetailsForm; |
| import AppBar from '@mui/material/AppBar'; | import AppBar from '@mui/material/AppBar'; | ||||
| import Box from '@mui/material/Box'; | import Box from '@mui/material/Box'; | ||||
| import Typography from '@mui/material/Typography'; | import Typography from '@mui/material/Typography'; | ||||
| import { signOut, useSession } from 'next-auth/react'; | |||||
| import Image from 'next/image'; | import Image from 'next/image'; | ||||
| import Link from 'next/link'; | import Link from 'next/link'; | ||||
| import { useRouter } from 'next/router'; | import { useRouter } from 'next/router'; | ||||
| const Navbar = () => { | const Navbar = () => { | ||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { totalQuantity } = useStore(); | const { totalQuantity } = useStore(); | ||||
| const { data: session } = useSession(); | |||||
| const signOutHandler = async () => { | |||||
| const data = await signOut({ redirect: false, callbackUrl: '/' }); | |||||
| router.push(data.url); | |||||
| }; | |||||
| return ( | return ( | ||||
| <AppBar | <AppBar | ||||
| position="absolute" | position="absolute" | ||||
| flexGrow: 1, | flexGrow: 1, | ||||
| maxWidth: '50%', | maxWidth: '50%', | ||||
| height: 30, | height: 30, | ||||
| display: { xs: 'none', md: 'flex' }, | |||||
| display: 'flex', | |||||
| px: 10, | px: 10, | ||||
| }} | }} | ||||
| > | > | ||||
| textAlign="center" | textAlign="center" | ||||
| sx={{ | sx={{ | ||||
| mx: 'auto', | mx: 'auto', | ||||
| fontSize: 20, | |||||
| fontSize: { md: 20, xs: 17 }, | |||||
| fontWeight: 500, | fontWeight: 500, | ||||
| mr: { lg: 0, xs: 2 }, | |||||
| color: router.pathname === '/' ? 'white' : 'black', | color: router.pathname === '/' ? 'white' : 'black', | ||||
| textDecoration: 'none', | textDecoration: 'none', | ||||
| cursor: 'pointer', | cursor: 'pointer', | ||||
| textAlign="center" | textAlign="center" | ||||
| sx={{ | sx={{ | ||||
| mx: 'auto', | mx: 'auto', | ||||
| fontSize: 20, | |||||
| fontSize: { md: 20, xs: 17 }, | |||||
| fontWeight: 500, | fontWeight: 500, | ||||
| mr: { lg: 0, xs: 2 }, | |||||
| color: router.pathname === '/' ? 'white' : 'black', | color: router.pathname === '/' ? 'white' : 'black', | ||||
| textDecoration: 'none', | textDecoration: 'none', | ||||
| cursor: 'pointer', | cursor: 'pointer', | ||||
| textAlign="center" | textAlign="center" | ||||
| sx={{ | sx={{ | ||||
| mx: 'auto', | mx: 'auto', | ||||
| fontSize: 20, | |||||
| fontSize: { md: 20, xs: 17 }, | |||||
| fontWeight: 500, | fontWeight: 500, | ||||
| mr: { lg: 0, xs: 2 }, | |||||
| color: router.pathname === '/' ? 'white' : 'black', | color: router.pathname === '/' ? 'white' : 'black', | ||||
| textDecoration: 'none', | textDecoration: 'none', | ||||
| cursor: 'pointer', | cursor: 'pointer', | ||||
| textAlign="center" | textAlign="center" | ||||
| sx={{ | sx={{ | ||||
| mx: 'auto', | mx: 'auto', | ||||
| fontSize: 20, | |||||
| fontSize: { md: 20, xs: 17 }, | |||||
| fontWeight: 500, | fontWeight: 500, | ||||
| mr: { lg: 0, xs: 2 }, | |||||
| color: router.pathname === '/' ? 'white' : 'black', | color: router.pathname === '/' ? 'white' : 'black', | ||||
| textDecoration: 'none', | textDecoration: 'none', | ||||
| cursor: 'pointer', | cursor: 'pointer', | ||||
| textAlign="center" | textAlign="center" | ||||
| sx={{ | sx={{ | ||||
| mx: 'auto', | mx: 'auto', | ||||
| fontSize: 20, | |||||
| fontSize: { md: 20, xs: 17 }, | |||||
| fontWeight: 500, | fontWeight: 500, | ||||
| color: router.pathname === '/' ? 'white' : 'black', | color: router.pathname === '/' ? 'white' : 'black', | ||||
| textDecoration: 'none', | textDecoration: 'none', | ||||
| flexGrow: 1, | flexGrow: 1, | ||||
| maxWidth: '50%', | maxWidth: '50%', | ||||
| height: 30, | height: 30, | ||||
| display: { xs: 'none', md: 'flex' }, | |||||
| display: 'flex', | |||||
| justifyContent: 'right', | justifyContent: 'right', | ||||
| pt: 0.5, | pt: 0.5, | ||||
| mr: 4, | 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 | <Box | ||||
| sx={{ | sx={{ | ||||
| mx: 2, | mx: 2, | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| </Link> | </Link> | ||||
| , | |||||
| </Box> | </Box> | ||||
| </Box> | </Box> | ||||
| </Box> | </Box> |
| 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; |
| const { CircularProgress } = require('@mui/material'); | |||||
| const LoadingSpinner = () => { | |||||
| return <CircularProgress />; | |||||
| }; | |||||
| export default LoadingSpinner; |
| 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; |
| export const libraries = ['places']; | |||||
| export const mapContainerStyle = { | |||||
| width: '100%', | |||||
| height: '100%', | |||||
| }; | |||||
| export const center = { | |||||
| lat: 43.30920996410931, | |||||
| lng: 21.911334213495593, | |||||
| }; |
| import { Grid, Typography } from '@mui/material'; | import { Grid, Typography } from '@mui/material'; | ||||
| import { Box } from '@mui/system'; | 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 OrderCard from '../cards/order-card/OrderCard'; | ||||
| import ShippingDetailsForm from '../forms/shipping-details/ShippingDetailsForm'; | 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 ( | return ( | ||||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | ||||
| <Grid item xs={12}> | <Grid item xs={12}> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={8} sx={{ mt: 4 }}> | <Grid item xs={8} sx={{ mt: 4 }}> | ||||
| <Typography sx={{ pl: 12, fontSize: 20 }}> | <Typography sx={{ pl: 12, fontSize: 20 }}> | ||||
| Save details for later | |||||
| Save details for later (user will be logged out) | |||||
| </Typography> | </Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={4} sx={{ mt: 4 }}> | <Grid item xs={4} sx={{ mt: 4 }}> | ||||
| <Typography sx={{ fontSize: 20 }}>Previous Orders</Typography> | <Typography sx={{ fontSize: 20 }}>Previous Orders</Typography> | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={8}> | <Grid item xs={8}> | ||||
| <ShippingDetailsForm></ShippingDetailsForm> | |||||
| <ShippingDetailsForm | |||||
| submitHandler={updateUserHandler} | |||||
| enableBtn={enableBtn} | |||||
| ></ShippingDetailsForm> | |||||
| </Grid> | </Grid> | ||||
| <Grid item xs={4}> | <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> | ||||
| </Grid> | </Grid> | ||||
| ); | ); |
| import { Breadcrumbs, Button, Divider, Grid, Typography } from '@mui/material'; | |||||
| import { Button, Grid, Typography } from '@mui/material'; | |||||
| import { Box } from '@mui/system'; | 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 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 ( | return ( | ||||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | <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 }}> | <Grid item xs={12} sx={{ mt: 1 }}> | ||||
| <Typography | <Typography | ||||
| sx={{ | sx={{ | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | ||||
| Order placed on: 05/09/2022 | |||||
| Order placed on: {orderData.time} | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | ||||
| Email: johndoe@test | |||||
| Email: {orderData?.shippingAddress?.email} | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | ||||
| Total: $60 | |||||
| Total: ${orderData?.totalPrice} | |||||
| </Typography> | </Typography> | ||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| }} | }} | ||||
| > | > | ||||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}> | <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> | </Typography> | ||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| mr: 2, | mr: 2, | ||||
| fontSize: 16, | fontSize: 16, | ||||
| }} | }} | ||||
| onClick={() => { | |||||
| router.push('/'); | |||||
| }} | |||||
| > | > | ||||
| Back to Home | Back to Home | ||||
| </Button> | </Button> |
| 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 { Box } from '@mui/system'; | ||||
| 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 DataCardS from '../cards/data-card-shipping/DataCardS'; | |||||
| import DataCard from '../cards/data-card/DataCard'; | 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 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 ( | return ( | ||||
| <Grid container spacing={2} sx={{ py: 10, height: '100%', width: '100%' }}> | <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 }}> | <Grid item xs={12} sx={{ mt: 1 }}> | ||||
| <Typography sx={{ pl: 12, fontSize: 20 }}> | <Typography sx={{ pl: 12, fontSize: 20 }}> | ||||
| The following fields will be used as the shipping details for your | The following fields will be used as the shipping details for your | ||||
| order | order | ||||
| </Typography> | </Typography> | ||||
| </Grid> | </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>johndoe@test.com | 0601234567</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>1684 Upton Avenue | Locke Mills</Typography> | |||||
| <Button | |||||
| sx={{ | |||||
| height: 35, | |||||
| width: 125, | |||||
| fontSize: 15, | |||||
| textTransform: 'none', | |||||
| backgroundColor: '#CBA213', | |||||
| color: 'white', | |||||
| }} | |||||
| > | |||||
| Change | |||||
| </Button> | |||||
| </Box> | |||||
| <Grid item xs={12} lg={8}> | |||||
| <ShippingData | |||||
| email={checkoutStorage?.userInfo?.email} | |||||
| address={checkoutStorage?.userInfo?.address} | |||||
| city={checkoutStorage?.userInfo?.city} | |||||
| postcode={checkoutStorage?.userInfo?.postcode} | |||||
| handleOpen={handleOpen} | |||||
| /> | |||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| display: 'flex', | display: 'flex', | ||||
| sx={{ color: 'black', ml: 2 }} | sx={{ color: 'black', ml: 2 }} | ||||
| /> | /> | ||||
| </Box> | </Box> | ||||
| <ButtonGroup | |||||
| handleStripePayment={handleStripePayment} | |||||
| handleBackToCart={handleBackToCart} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12} lg={4}> | |||||
| <Box | <Box | ||||
| sx={{ | sx={{ | ||||
| display: 'flex', | |||||
| ml: 12, | |||||
| mb: 2, | |||||
| borderRadius: 2, | |||||
| p: 1, | |||||
| width: '80%', | |||||
| mt: 2, | |||||
| height: '100%', | |||||
| ml: { xs: 12, lg: 0 }, | |||||
| display: { lg: 'block', xs: 'flex' }, | |||||
| flexWrap: { xs: 'wrap', lg: 'none' }, | |||||
| justifyContent: { xs: 'center', lg: 'none' }, | |||||
| }} | }} | ||||
| > | > | ||||
| <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> | |||||
| </Grid> | |||||
| <Grid item xs={4}> | |||||
| <Box sx={{ width: '80%', mt: 2 }}> | |||||
| <DataCard></DataCard> | |||||
| <DataCard></DataCard> | |||||
| <DataCard></DataCard> | |||||
| {mapProductsToDom()} | |||||
| </Box> | </Box> | ||||
| </Grid> | </Grid> | ||||
| <ShippingModal | |||||
| open={open} | |||||
| handleClose={handleClose} | |||||
| handleChangeShipping={handleChangeShipping} | |||||
| handleChangeContact={handleChangeContact} | |||||
| /> | |||||
| </Grid> | </Grid> | ||||
| ); | ); | ||||
| }; | }; |
| 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; |
| 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: { lg: '90%', xs: '80%' }, | |||||
| borderRadius: 2, | |||||
| p: 1, | |||||
| }} | |||||
| > | |||||
| <Typography sx={{ fontSize: 18, fontWeight: 600 }}>Contact</Typography> | |||||
| <Typography>{email}</Typography> | |||||
| <Button | |||||
| sx={{ | |||||
| height: 35, | |||||
| minWidth: { md: 125, xs: 90 }, | |||||
| 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: { lg: '90%', xs: '80%' }, | |||||
| borderRadius: 2, | |||||
| p: 1, | |||||
| }} | |||||
| > | |||||
| <Typography | |||||
| sx={{ | |||||
| fontSize: { md: 18, xs: 16 }, | |||||
| fontWeight: 600, | |||||
| mr: { xs: 1, sm: 0 }, | |||||
| }} | |||||
| > | |||||
| Shipping to | |||||
| </Typography> | |||||
| <Typography> | |||||
| {address} | {city} | {postcode} | |||||
| </Typography> | |||||
| <Button | |||||
| sx={{ | |||||
| height: 35, | |||||
| minWidth: { md: 125, xs: 90 }, | |||||
| 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; |
| 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; |
| 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`, | |||||
| }); | |||||
| }; |
| const mongoose = require('mongoose'); | const mongoose = require('mongoose'); | ||||
| const validator = require('validator'); | const validator = require('validator'); | ||||
| const OrderSchema = new mongoose.Schema({ | |||||
| coffee: [ | |||||
| { | |||||
| name: { | |||||
| 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, | type: String, | ||||
| required: [true, 'Please provide a name.'], | |||||
| required: [true, 'Please provide a country.'], | |||||
| trim: true, | |||||
| }, | }, | ||||
| customID: { | |||||
| city: { | |||||
| type: String, | type: String, | ||||
| required: [true, 'Please provide a custom id.'], | |||||
| 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.'], | |||||
| }, | }, | ||||
| }, | }, | ||||
| ], | |||||
| time: { | |||||
| type: Date, | |||||
| required: [true, 'Please provide a date.'], | |||||
| validate(value) { | |||||
| if (!validator.isDate(value)) { | |||||
| throw new Error('Not a date'); | |||||
| } | |||||
| 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'); | |||||
| } | |||||
| }, | |||||
| }, | }, | ||||
| }, | |||||
| 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'); | |||||
| } | |||||
| }, | |||||
| }, | }, | ||||
| }, | |||||
| 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'); | |||||
| } | |||||
| 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.'], | |||||
| }, | }, | ||||
| }, | }, | ||||
| 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; | module.exports = Order; |
| const mongoose = require('mongoose'); | const mongoose = require('mongoose'); | ||||
| const validator = require('validator'); | 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', { | UserSchema.virtual('orders', { | ||||
| ref: 'Order', | ref: 'Order', | ||||
| city: user.city, | city: user.city, | ||||
| country: user.country, | country: user.country, | ||||
| postcode: user.postcode, | postcode: user.postcode, | ||||
| orders: user.orders, | |||||
| _id: user._id, | |||||
| }; | }; | ||||
| return userData; | return userData; | ||||
| }; | }; | ||||
| next(); | next(); | ||||
| }); | }); | ||||
| const User = mongoose.models.User || mongoose.model('User', UserSchema); | |||||
| const User = mongoose.models.User || mongoose.model('User', UserSchema, 'User'); | |||||
| module.exports = User; | module.exports = User; |
| images: { | images: { | ||||
| domains: ['www.business2community.com'], | 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, | reactStrictMode: true, | ||||
| swcMinify: true, | swcMinify: true, | ||||
| i18n, | i18n, |
| "@mui/codemod": "^5.8.7", | "@mui/codemod": "^5.8.7", | ||||
| "@mui/icons-material": "^5.8.4", | "@mui/icons-material": "^5.8.4", | ||||
| "@mui/material": "^5.9.2", | "@mui/material": "^5.9.2", | ||||
| "@react-google-maps/api": "^2.12.2", | |||||
| "@stripe/stripe-js": "^1.35.0", | |||||
| "@tanstack/react-query": "^4.0.10", | "@tanstack/react-query": "^4.0.10", | ||||
| "bcryptjs": "^2.4.3", | "bcryptjs": "^2.4.3", | ||||
| "date-fns": "^2.29.1", | "date-fns": "^2.29.1", | ||||
| "next": "12.2.3", | "next": "12.2.3", | ||||
| "next-auth": "^4.10.2", | "next-auth": "^4.10.2", | ||||
| "next-i18next": "^11.3.0", | "next-i18next": "^11.3.0", | ||||
| "nookies": "^2.5.2", | |||||
| "prop-types": "^15.8.1", | "prop-types": "^15.8.1", | ||||
| "react": "18.2.0", | "react": "18.2.0", | ||||
| "react-dom": "18.2.0", | "react-dom": "18.2.0", | ||||
| "sass": "^1.54.0", | "sass": "^1.54.0", | ||||
| "stripe": "^10.8.0", | |||||
| "swr": "^1.3.0", | "swr": "^1.3.0", | ||||
| "validator": "^13.7.0", | "validator": "^13.7.0", | ||||
| "yup": "^0.32.11" | "yup": "^0.32.11" |
| import Layout from '../components/layout/base-layout/Layout'; | import Layout from '../components/layout/base-layout/Layout'; | ||||
| import CircularIndeterminate from '../components/loader/route-loader/CircularIndeterminate'; | import CircularIndeterminate from '../components/loader/route-loader/CircularIndeterminate'; | ||||
| import StorageProvider from '../store/cart-context'; | import StorageProvider from '../store/cart-context'; | ||||
| import CheckoutProvider from '../store/checkout-context'; | |||||
| import '../styles/globals.css'; | import '../styles/globals.css'; | ||||
| import theme from '../styles/muiTheme'; | import theme from '../styles/muiTheme'; | ||||
| const Providers = ({ components, children }) => ( | |||||
| <> | |||||
| {components.reduceRight( | |||||
| (acc, Comp) => ( | |||||
| <Comp>{acc}</Comp> | |||||
| ), | |||||
| children | |||||
| )} | |||||
| </> | |||||
| ); | |||||
| function MyApp({ Component, pageProps: { session, ...pageProps } }) { | function MyApp({ Component, pageProps: { session, ...pageProps } }) { | ||||
| const [queryClient] = useState(() => new QueryClient()); | const [queryClient] = useState(() => new QueryClient()); | ||||
| <Hydrate state={pageProps.dehydratedState}> | <Hydrate state={pageProps.dehydratedState}> | ||||
| <SessionProvider session={session}> | <SessionProvider session={session}> | ||||
| <ThemeProvider theme={theme}> | <ThemeProvider theme={theme}> | ||||
| <StorageProvider> | |||||
| <Providers components={[CheckoutProvider, StorageProvider]}> | |||||
| <Layout> | <Layout> | ||||
| <Head> | <Head> | ||||
| <title>Coffee Shop</title> | <title>Coffee Shop</title> | ||||
| <CircularIndeterminate /> | <CircularIndeterminate /> | ||||
| <Component {...pageProps} /> | <Component {...pageProps} /> | ||||
| </Layout> | </Layout> | ||||
| </StorageProvider> | |||||
| </Providers> | |||||
| </ThemeProvider> | </ThemeProvider> | ||||
| </SessionProvider> | </SessionProvider> | ||||
| <ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools> | <ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools> |
| const Order = require('../../../models/order'); | const Order = require('../../../models/order'); | ||||
| import dbConnect from '../../../utils/helpers/dbHelpers'; | import dbConnect from '../../../utils/helpers/dbHelpers'; | ||||
| const mongoose = require('mongoose'); | |||||
| async function handler(req, res) { | async function handler(req, res) { | ||||
| const { method } = req; | const { method } = req; | ||||
| const ownerID = req.query.ownerID; | |||||
| await dbConnect(); | await dbConnect(); | ||||
| switch (method) { | switch (method) { | ||||
| .status(201) | .status(201) | ||||
| .json({ message: 'Your order was submitted successfully!', order }); | .json({ message: 'Your order was submitted successfully!', order }); | ||||
| } catch (error) { | } 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; | break; | ||||
| } | } |
| switch (method) { | switch (method) { | ||||
| case 'GET': { | case 'GET': { | ||||
| try { | try { | ||||
| const featuredProducts = await Product.find({ isFeatured: false }); | |||||
| const featuredProducts = await Product.find({ isFeatured: true }); | |||||
| if (!featuredProducts) { | if (!featuredProducts) { | ||||
| res.status(200).json({ | res.status(200).json({ |
| 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; |
| import nookies from 'nookies'; | |||||
| import CheckoutContent from '../../components/checkout-content/CheckoutContent'; | import CheckoutContent from '../../components/checkout-content/CheckoutContent'; | ||||
| const CheckoutPage = () => { | const CheckoutPage = () => { | ||||
| return <CheckoutContent></CheckoutContent>; | 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; | export default CheckoutPage; |
| 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 ProfileContent from '../../components/profile-content/ProfileContent'; | ||||
| import { LOGIN_PAGE } from '../../constants/pages'; | 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) { | export async function getServerSideProps(context) { | ||||
| }; | }; | ||||
| } | } | ||||
| const orders = await getOrdersForOwner(session.user._id); | |||||
| return { | return { | ||||
| props: { session }, | |||||
| props: { orders }, | |||||
| }; | }; | ||||
| } | } | ||||
| import nookies from 'nookies'; | |||||
| import ReviewContent from '../../components/review-content/ReviewContent'; | import ReviewContent from '../../components/review-content/ReviewContent'; | ||||
| const ReviewPage = () => { | const ReviewPage = () => { | ||||
| return <ReviewContent></ReviewContent>; | 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; | export default ReviewPage; |
| import nookies from 'nookies'; | |||||
| import ShippingContent from '../../components/shipping-content/ShippingContent'; | import ShippingContent from '../../components/shipping-content/ShippingContent'; | ||||
| const ShippingPage = () => { | const ShippingPage = () => { | ||||
| return <ShippingContent></ShippingContent>; | 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; | export default ShippingPage; |
| <?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> |
| productsByCategory: '/api/product/category/', | productsByCategory: '/api/product/category/', | ||||
| featuredProducts: '/api/product/featured-products', | featuredProducts: '/api/product/featured-products', | ||||
| order: '/api/order', | order: '/api/order', | ||||
| userUpdate: '/api/user', | |||||
| }; | }; |
| 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; | |||||
| }; |
| 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; | |||||
| }; |
| import * as Yup from 'yup'; | import * as Yup from 'yup'; | ||||
| export const contactSchema = Yup.object().shape({ | 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'), | email: Yup.string().email('Enter valid email').required('Email is required'), | ||||
| message: Yup.string().required('Message is required'), | |||||
| }); | }); |
| const clearCart = () => { | const clearCart = () => { | ||||
| setStorage(CART_KEY, []); | setStorage(CART_KEY, []); | ||||
| setTotalQuantity(0); | |||||
| setTotalPrice(0); | |||||
| setCartStorage([]); | setCartStorage([]); | ||||
| }; | }; | ||||
| import { createContext, useContext, useState } from 'react'; | |||||
| import { getSStorage, setSStorage } from '../utils/helpers/storage'; | |||||
| const CheckoutContext = createContext({ | |||||
| checkoutStorage: {}, | |||||
| }); | |||||
| const CheckoutDispatchContext = createContext({ | |||||
| addCheckoutValue: (products, userInfo, userID) => {}, | |||||
| changeContact: (email) => {}, | |||||
| changeShippingData: (shippingData) => {}, | |||||
| clearCheckout: () => {}, | |||||
| parseCheckoutValue: () => {}, | |||||
| }); | |||||
| export const useCheckoutData = () => { | |||||
| return useContext(CheckoutContext); | |||||
| }; | |||||
| export const useCheckoutDataUpdate = () => { | |||||
| return useContext(CheckoutDispatchContext); | |||||
| }; | |||||
| const useCheckout = () => { | |||||
| const CHECKOUT_KEY = 'checkout-data'; | |||||
| const [checkoutStorage, setCheckoutStorage] = useState( | |||||
| getSStorage(CHECKOUT_KEY) | |||||
| ); | |||||
| const addCheckoutValue = (products, userInfo, userID) => { | |||||
| setSStorage(CHECKOUT_KEY, { products, userInfo, userID }); | |||||
| setCheckoutStorage({ products, userInfo, userID }); | |||||
| }; | |||||
| const clearCheckout = () => { | |||||
| setSStorage(CHECKOUT_KEY, {}); | |||||
| 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, | |||||
| }; | |||||
| }; | |||||
| const CheckoutProvider = ({ children }) => { | |||||
| const { | |||||
| checkoutStorage, | |||||
| setCheckoutStorage, | |||||
| addCheckoutValue, | |||||
| clearCheckout, | |||||
| parseCheckoutValue, | |||||
| changeContact, | |||||
| changeShippingData, | |||||
| } = useCheckout(); | |||||
| return ( | |||||
| <CheckoutContext.Provider value={{ checkoutStorage }}> | |||||
| <CheckoutDispatchContext.Provider | |||||
| value={{ | |||||
| setCheckoutStorage, | |||||
| addCheckoutValue, | |||||
| clearCheckout, | |||||
| parseCheckoutValue, | |||||
| changeContact, | |||||
| changeShippingData, | |||||
| }} | |||||
| > | |||||
| {children} | |||||
| </CheckoutDispatchContext.Provider> | |||||
| </CheckoutContext.Provider> | |||||
| ); | |||||
| }; | |||||
| export default CheckoutProvider; |
| } | } | ||||
| window.localStorage.removeItem(key); | window.localStorage.removeItem(key); | ||||
| }; | }; | ||||
| export const setSStorage = (key, value) => { | |||||
| window.sessionStorage.setItem(key, JSON.stringify(value)); | |||||
| }; | |||||
| export const getSStorage = (key) => { | |||||
| if (typeof window === 'undefined') { | |||||
| return null; | |||||
| } | |||||
| const storedItems = window.sessionStorage.getItem(key); | |||||
| return storedItems ? JSON.parse(storedItems) : []; | |||||
| }; | |||||
| export const removeSStorage = (key) => { | |||||
| if (typeof window === 'undefined') { | |||||
| return null; | |||||
| } | |||||
| window.sessionStorage.removeItem(key); | |||||
| }; |
| 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`, | |||||
| }); | |||||
| } |