Bläddra i källkod

Merge branch 'master' of http://176.104.105.124:3000/ntasicc/coffee into single-product

single-product
Lazar Kostic 3 år sedan
förälder
incheckning
cf23f12319
44 ändrade filer med 46722 tillägg och 9477 borttagningar
  1. 8
    5
      components/cards/cart-card/CartCard.jsx
  2. 50
    9
      components/cards/data-card/DataCard.jsx
  3. 1
    1
      components/cards/order-card/OrderCard.jsx
  4. 13
    0
      components/cards/order-summary-card/OrderSummaryCard.jsx
  5. 19
    27
      components/cart-content/CartContent.jsx
  6. 60
    29
      components/checkout-content/CheckoutContent.jsx
  7. 67
    105
      components/forms/contact/ContactForm.jsx
  8. 30
    13
      components/forms/shipping-details/ShippingDetailsForm.jsx
  9. 36
    8
      components/layout/navbar/Navbar.jsx
  10. 46
    0
      components/layout/steps-title/StepTitle.jsx
  11. 7
    0
      components/loader/basic-spinner/LoadSpinner.jsx
  12. 34
    0
      components/map/Map.jsx
  13. 10
    0
      components/map/MapConst.js
  14. 38
    14
      components/profile-content/ProfileContent.jsx
  15. 50
    30
      components/review-content/ReviewContent.jsx
  16. 95
    136
      components/shipping-content/ShippingContent.jsx
  17. 56
    0
      components/shipping-content/shipping-btnGroup/ButtonGroup.jsx
  18. 93
    0
      components/shipping-content/shipping-data/ShippingData.jsx
  19. 47
    0
      components/shipping-content/shipping-modal/ShippingModal.jsx
  20. 21
    0
      hooks/use-stripe.js
  21. 133
    47
      models/order.js
  22. 67
    59
      models/user.js
  23. 5
    0
      next.config.js
  24. 36100
    0
      package-lock.json
  25. 4
    0
      package.json
  26. 14
    2
      pages/_app.js
  27. 0
    0
      pages/api/order/[orderID].js
  28. 25
    2
      pages/api/order/index.js
  29. 1
    1
      pages/api/product/featured-products.js
  30. 46
    0
      pages/api/user/index.js
  31. 18
    0
      pages/checkout/index.js
  32. 7
    19
      pages/profile/index.js
  33. 18
    0
      pages/review/index.js
  34. 17
    0
      pages/shipping/index.js
  35. 57
    0
      public/images/logout.svg
  36. 1
    0
      requests/apiEndpoints.js
  37. 15
    0
      requests/orders/getOrdersForOwnerRequest.js
  38. 20
    0
      requests/user/userUpdateRequest.js
  39. 0
    3
      schemas/contactSchema.js
  40. 2
    0
      store/cart-context.js
  41. 120
    0
      store/checkout-context.js
  42. 21
    0
      utils/helpers/storage.js
  43. 23
    0
      utils/helpers/stripe.js
  44. 9227
    8967
      yarn.lock

+ 8
- 5
components/cards/cart-card/CartCard.jsx Visa fil

@@ -14,15 +14,16 @@ const CartCard = ({ product, initialQuantity, remove, updateQuantity }) => {
<Paper
sx={{
p: 1,
width: '88%',
width: { lg: '88%', xs: '73%', md: '80%' },
mb: 2,
ml: 12,
backgroundColor: '#f2f2f2',
display: 'flex',
justifyContent: { xs: 'center', md: 'none' },
}}
elevation={3}
>
<Box sx={{ width: '30%' }}>
<Box sx={{ width: '30%', display: { xs: 'none', md: 'block' } }}>
<Image
src="/images/coffee-mug.svg"
alt="profile"
@@ -45,7 +46,8 @@ const CartCard = ({ product, initialQuantity, remove, updateQuantity }) => {
textAlign: 'center',
height: 25,
fontWeight: 600,
fontSize: 20,
ml: { xs: -1, sm: 0 },
fontSize: { xs: 16, sm: 20 },
}}
>
{product?.name}
@@ -58,6 +60,7 @@ const CartCard = ({ product, initialQuantity, remove, updateQuantity }) => {
width: '20%',
justifyContent: 'center',
alignItems: 'center',
ml: { xs: 2, sm: 0 },
}}
>
<Typography
@@ -140,7 +143,7 @@ const CartCard = ({ product, initialQuantity, remove, updateQuantity }) => {
</Box>
<Box
sx={{
ml: 3,
ml: { xs: 5, sm: 3 },
display: 'flex',
flexDirection: 'column',
width: '20%',
@@ -153,7 +156,7 @@ const CartCard = ({ product, initialQuantity, remove, updateQuantity }) => {
width: '100%',
textAlign: 'center',
height: 25,
fontSize: 18,
fontSize: { xs: 15, md: 18 },
}}
>
Price: ${product?.price}

+ 50
- 9
components/cards/data-card/DataCard.jsx Visa fil

@@ -1,19 +1,29 @@
import { Box, Paper, Typography } from '@mui/material';
import Image from 'next/image';
import PropType from 'prop-types';

const DataCard = () => {
const DataCard = ({ data, quantity }) => {
return (
<Paper
sx={{
p: 3,
width: '100%',
width: { lg: '100%', xs: '35%' },
mb: 2,
ml: { lg: 0, xs: 6 },
backgroundColor: '#f2f2f2',
display: 'flex',
flex: { xs: [0, 0, '32%'], lg: 'none' },
}}
elevation={3}
>
<Box sx={{ width: '30%', borderRadius: 4, overflow: 'hidden' }}>
<Box
sx={{
width: '30%',
borderRadius: 4,
overflow: 'hidden',
display: { xs: 'none', lg: 'block' },
}}
>
<Image
src="/images/coffee-mug.svg"
alt="profile"
@@ -22,7 +32,12 @@ const DataCard = () => {
/>
</Box>
<Box
sx={{ ml: 3, display: 'flex', flexDirection: 'column', width: '60%' }}
sx={{
ml: 3,
display: 'flex',
flexDirection: 'column',
width: { lg: '60%', sx: '100%' },
}}
>
<Typography
sx={{
@@ -30,23 +45,49 @@ const DataCard = () => {
textAlign: 'center',
height: 25,
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
sx={{
mt: 3,
mt: { lg: 3, xs: 1 },
textAlign: 'right',
fontSize: 14,
}}
>
Simple and beautiful Begin mug. Perfect companion for your next
delicious cup of coffee.
${data.price} (per unit)
</Typography>
</Box>
</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;

+ 1
- 1
components/cards/order-card/OrderCard.jsx Visa fil

@@ -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>

+ 13
- 0
components/cards/order-summary-card/OrderSummaryCard.jsx Visa fil

@@ -1,9 +1,12 @@
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 }) => {
const router = useRouter();
return (
<Paper
sx={{ p: 3, width: '100%', mb: 2, backgroundColor: '#f1f1f1' }}
@@ -36,6 +39,15 @@ const OrderSummaryCard = ({ data }) => {
startIcon={
<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
</Button>
@@ -51,6 +63,7 @@ const OrderSummaryCard = ({ data }) => {
OrderSummaryCard.propTypes = {
data: PropType.shape({
totalPrice: PropType.number,
totalQuantity: PropType.number,
}),
};


+ 19
- 27
components/cart-content/CartContent.jsx Visa fil

@@ -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 } = useStore();
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,36 +44,19 @@ 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 }}
data={{ totalPrice: totalPrice, totalQuantity: totalQuantity }}
></OrderSummaryCard>
</Box>
</Grid>

+ 60
- 29
components/checkout-content/CheckoutContent.jsx Visa fil

@@ -1,46 +1,77 @@
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();

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 (
<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
order
</Typography>
</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 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>
</Grid>
</Grid>

+ 67
- 105
components/forms/contact/ContactForm.jsx Visa fil

@@ -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;

+ 30
- 13
components/forms/shipping-details/ShippingDetailsForm.jsx Visa fil

@@ -1,36 +1,44 @@
import { Box, Button, Paper, TextField } from '@mui/material';
import { useFormik } from 'formik';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import PropType from 'prop-types';
import { useState } from 'react';
import { shippingDetailsSchema } from '../../../schemas/shippingDetailsSchema';
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 { data: session } = useSession();
const submitHandler = async (values) => {
console.log(values);
const router = useRouter();

const formikSubmitHandler = async (values) => {
submitHandler(values);
};

const formik = useFormik({
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,
onSubmit: submitHandler,
onSubmit: formikSubmitHandler,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Paper
sx={{ p: 3, width: '90%', ml: 12, backgroundColor: '#f2f2f2' }}
sx={{ p: 3, width: '90%', ml: 12, mt: 2, backgroundColor: '#f2f2f2' }}
elevation={3}
>
<Box
@@ -123,6 +131,9 @@ const ShippingDetailsForm = ({ backBtn = false }) => {
color: 'white',
mr: 2,
}}
onClick={() => {
router.push('/cart');
}}
>
Back to cart
</Button>
@@ -135,12 +146,16 @@ const ShippingDetailsForm = ({ backBtn = false }) => {
mb: 2,
backgroundColor: '#CBA213',
height: 50,
width: 150,
width: isCheckout ? 200 : 150,
textTransform: 'none',
color: 'white',
}}
disabled={!enableBtn}
onClick={() => {
submitHandler;
}}
>
Submit Details
{isCheckout ? 'Proceed to shipping' : 'Submit Details'}
</Button>
</Box>
</Box>
@@ -150,6 +165,8 @@ const ShippingDetailsForm = ({ backBtn = false }) => {

ShippingDetailsForm.propTypes = {
backBtn: PropType.Boolean,
isCheckout: PropType.Boolean,
submitHandler: PropType.func,
};

export default ShippingDetailsForm;

+ 36
- 8
components/layout/navbar/Navbar.jsx Visa fil

@@ -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"
@@ -33,7 +41,7 @@ const Navbar = () => {
flexGrow: 1,
maxWidth: '50%',
height: 30,
display: { xs: 'none', md: 'flex' },
display: 'flex',
px: 10,
}}
>
@@ -42,8 +50,9 @@ const Navbar = () => {
textAlign="center"
sx={{
mx: 'auto',
fontSize: 20,
fontSize: { md: 20, xs: 17 },
fontWeight: 500,
mr: { lg: 0, xs: 2 },
color: router.pathname === '/' ? 'white' : 'black',
textDecoration: 'none',
cursor: 'pointer',
@@ -58,8 +67,9 @@ const Navbar = () => {
textAlign="center"
sx={{
mx: 'auto',
fontSize: 20,
fontSize: { md: 20, xs: 17 },
fontWeight: 500,
mr: { lg: 0, xs: 2 },
color: router.pathname === '/' ? 'white' : 'black',
textDecoration: 'none',
cursor: 'pointer',
@@ -74,8 +84,9 @@ const Navbar = () => {
textAlign="center"
sx={{
mx: 'auto',
fontSize: 20,
fontSize: { md: 20, xs: 17 },
fontWeight: 500,
mr: { lg: 0, xs: 2 },
color: router.pathname === '/' ? 'white' : 'black',
textDecoration: 'none',
cursor: 'pointer',
@@ -90,8 +101,9 @@ const Navbar = () => {
textAlign="center"
sx={{
mx: 'auto',
fontSize: 20,
fontSize: { md: 20, xs: 17 },
fontWeight: 500,
mr: { lg: 0, xs: 2 },
color: router.pathname === '/' ? 'white' : 'black',
textDecoration: 'none',
cursor: 'pointer',
@@ -106,7 +118,7 @@ const Navbar = () => {
textAlign="center"
sx={{
mx: 'auto',
fontSize: 20,
fontSize: { md: 20, xs: 17 },
fontWeight: 500,
color: router.pathname === '/' ? 'white' : 'black',
textDecoration: 'none',
@@ -122,12 +134,29 @@ const Navbar = () => {
flexGrow: 1,
maxWidth: '50%',
height: 30,
display: { xs: 'none', md: 'flex' },
display: 'flex',
justifyContent: 'right',
pt: 0.5,
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 +207,6 @@ const Navbar = () => {
/>
</Box>
</Link>
,
</Box>
</Box>
</Box>

+ 46
- 0
components/layout/steps-title/StepTitle.jsx Visa fil

@@ -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;

+ 7
- 0
components/loader/basic-spinner/LoadSpinner.jsx Visa fil

@@ -0,0 +1,7 @@
const { CircularProgress } = require('@mui/material');

const LoadingSpinner = () => {
return <CircularProgress />;
};

export default LoadingSpinner;

+ 34
- 0
components/map/Map.jsx Visa fil

@@ -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;

+ 10
- 0
components/map/MapConst.js Visa fil

@@ -0,0 +1,10 @@
export const libraries = ['places'];

export const mapContainerStyle = {
width: '100%',
height: '100%',
};
export const center = {
lat: 43.30920996410931,
lng: 21.911334213495593,
};

+ 38
- 14
components/profile-content/ProfileContent.jsx Visa fil

@@ -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>
);

+ 50
- 30
components/review-content/ReviewContent.jsx Visa fil

@@ -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>

+ 95
- 136
components/shipping-content/ShippingContent.jsx Visa fil

@@ -1,109 +1,90 @@
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 { 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 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
order
</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>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
sx={{
display: 'flex',
@@ -123,54 +104,32 @@ const ShippingContent = () => {
sx={{ color: 'black', ml: 2 }}
/>
</Box>
<ButtonGroup
handleStripePayment={handleStripePayment}
handleBackToCart={handleBackToCart}
/>
</Grid>
<Grid item xs={12} lg={4}>
<Box
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>
</Grid>
<ShippingModal
open={open}
handleClose={handleClose}
handleChangeShipping={handleChangeShipping}
handleChangeContact={handleChangeContact}
/>
</Grid>
);
};

+ 56
- 0
components/shipping-content/shipping-btnGroup/ButtonGroup.jsx Visa fil

@@ -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;

+ 93
- 0
components/shipping-content/shipping-data/ShippingData.jsx Visa fil

@@ -0,0 +1,93 @@
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;

+ 47
- 0
components/shipping-content/shipping-modal/ShippingModal.jsx Visa fil

@@ -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;

+ 21
- 0
hooks/use-stripe.js Visa fil

@@ -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`,
});
};

+ 133
- 47
models/order.js Visa fil

@@ -1,62 +1,148 @@
const mongoose = require('mongoose');
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,
required: [true, 'Please provide a name.'],
required: [true, 'Please provide a country.'],
trim: true,
},
customID: {
city: {
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;

+ 67
- 59
models/user.js Visa fil

@@ -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,8 @@ UserSchema.statics.findByCredentials = async (username, password) => {
city: user.city,
country: user.country,
postcode: user.postcode,
orders: user.orders,
_id: user._id,
};
return userData;
};
@@ -109,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
- 0
next.config.js Visa fil

@@ -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,

+ 36100
- 0
package-lock.json
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


+ 4
- 0
package.json Visa fil

@@ -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"

+ 14
- 2
pages/_app.js Visa fil

@@ -12,9 +12,21 @@ import { useState } from 'react';
import Layout from '../components/layout/base-layout/Layout';
import CircularIndeterminate from '../components/loader/route-loader/CircularIndeterminate';
import StorageProvider from '../store/cart-context';
import CheckoutProvider from '../store/checkout-context';
import '../styles/globals.css';
import theme from '../styles/muiTheme';

const Providers = ({ components, children }) => (
<>
{components.reduceRight(
(acc, Comp) => (
<Comp>{acc}</Comp>
),
children
)}
</>
);

function MyApp({ Component, pageProps: { session, ...pageProps } }) {
const [queryClient] = useState(() => new QueryClient());

@@ -23,7 +35,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
<Hydrate state={pageProps.dehydratedState}>
<SessionProvider session={session}>
<ThemeProvider theme={theme}>
<StorageProvider>
<Providers components={[CheckoutProvider, StorageProvider]}>
<Layout>
<Head>
<title>Coffee Shop</title>
@@ -36,7 +48,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
<CircularIndeterminate />
<Component {...pageProps} />
</Layout>
</StorageProvider>
</Providers>
</ThemeProvider>
</SessionProvider>
<ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools>

+ 0
- 0
pages/api/order/[orderID].js Visa fil


+ 25
- 2
pages/api/order/index.js Visa fil

@@ -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;
}

+ 1
- 1
pages/api/product/featured-products.js Visa fil

@@ -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({

+ 46
- 0
pages/api/user/index.js Visa fil

@@ -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;

+ 18
- 0
pages/checkout/index.js Visa fil

@@ -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;

+ 7
- 19
pages/profile/index.js Visa fil

@@ -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 },
};
}


+ 18
- 0
pages/review/index.js Visa fil

@@ -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;

+ 17
- 0
pages/shipping/index.js Visa fil

@@ -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;

+ 57
- 0
public/images/logout.svg Visa fil

@@ -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>

+ 1
- 0
requests/apiEndpoints.js Visa fil

@@ -7,4 +7,5 @@ export default {
productsByCategory: '/api/product/category/',
featuredProducts: '/api/product/featured-products',
order: '/api/order',
userUpdate: '/api/user',
};

+ 15
- 0
requests/orders/getOrdersForOwnerRequest.js Visa fil

@@ -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;
};

+ 20
- 0
requests/user/userUpdateRequest.js Visa fil

@@ -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;
};

+ 0
- 3
schemas/contactSchema.js Visa fil

@@ -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'),
});

+ 2
- 0
store/cart-context.js Visa fil

@@ -101,6 +101,8 @@ const useStorage = () => {

const clearCart = () => {
setStorage(CART_KEY, []);
setTotalQuantity(0);
setTotalPrice(0);
setCartStorage([]);
};


+ 120
- 0
store/checkout-context.js Visa fil

@@ -0,0 +1,120 @@
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;

+ 21
- 0
utils/helpers/storage.js Visa fil

@@ -18,3 +18,24 @@ export const removeStorage = (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);
};

+ 23
- 0
utils/helpers/stripe.js Visa fil

@@ -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`,
});
}

+ 9227
- 8967
yarn.lock
Filskillnaden har hållits tillbaka eftersom den är för stor
Visa fil


Laddar…
Avbryt
Spara