瀏覽代碼

Merge branch 'front-typescript' of ntasicc/coffee-ts into master

ts-refactor
lazarkostic 3 年之前
父節點
當前提交
3f0ddd31ad
共有 100 個文件被更改,包括 4930 次插入70 次删除
  1. 51
    0
      components/buttons/load-more/LoadMore.tsx
  2. 24
    0
      components/cards/card-container/CardContainer.tsx
  3. 176
    0
      components/cards/cart-card/CartCard.tsx
  4. 69
    0
      components/cards/data-card/DataCard.tsx
  5. 44
    0
      components/cards/order-card/OrderCard.tsx
  6. 73
    0
      components/cards/order-summary-card/OrderSummaryCard.tsx
  7. 74
    0
      components/cart-content/CartContent.tsx
  8. 76
    0
      components/checkout-content/CheckoutContent.tsx
  9. 120
    0
      components/company-info/CompanyInfo.tsx
  10. 22
    0
      components/empty-cart/EmptyCart.tsx
  11. 35
    0
      components/features/FeatureItem.tsx
  12. 89
    0
      components/features/Features.tsx
  13. 23
    0
      components/features/items.ts
  14. 29
    0
      components/filter-sort/FilterSort.tsx
  15. 75
    0
      components/forms/contact/ContactForm.tsx
  16. 135
    0
      components/forms/contact/ContactPageForm.tsx
  17. 83
    0
      components/forms/forgot-password/ForgotPasswordForm.tsx
  18. 155
    0
      components/forms/login/LoginForm.tsx
  19. 263
    0
      components/forms/register/RegisterForm.tsx
  20. 164
    0
      components/forms/shipping-details/ShippingDetailsForm.tsx
  21. 11
    0
      components/grid-item/GridItem.tsx
  22. 142
    0
      components/hero/Hero.tsx
  23. 23
    0
      components/layout/base-layout/Layout.tsx
  24. 18
    0
      components/layout/content-wrapper/ContentContainer.tsx
  25. 148
    0
      components/layout/footer/Footer.tsx
  26. 127
    0
      components/layout/navbar/DesktopNav.tsx
  27. 105
    0
      components/layout/navbar/MainNav.tsx
  28. 135
    0
      components/layout/navbar/MobileNav.tsx
  29. 87
    0
      components/layout/navbar/NavItem.tsx
  30. 29
    0
      components/layout/navbar/navItems.tsx
  31. 7
    0
      components/layout/page-wrapper/PageWrapper.tsx
  32. 55
    0
      components/layout/steps-title/StepTitle.tsx
  33. 35
    0
      components/loader/Loader.tsx
  34. 11
    0
      components/loader/basic-spinner/LoadSpinner.tsx
  35. 56
    0
      components/loader/route-loader/CircularIndeterminate.tsx
  36. 15
    0
      components/mui/ErrorMessageComponent.tsx
  37. 22
    0
      components/notification/Notification.tsx
  38. 12
    0
      components/page-description/PageDescription.tsx
  39. 105
    0
      components/product-card/ProductCard.tsx
  40. 29
    0
      components/product-type/ProductType.tsx
  41. 60
    0
      components/products-content/ProductsContent.tsx
  42. 43
    0
      components/products-grid/ProductsGrid.tsx
  43. 43
    0
      components/products-hero/ProductsHero.tsx
  44. 78
    0
      components/products/featured-product/FeaturedProduct.tsx
  45. 19
    0
      components/products/featured-product/ProductImage.tsx
  46. 154
    0
      components/products/featured-product/ProductInfo.tsx
  47. 27
    0
      components/products/featured-products-list/FeaturedPorductsList.tsx
  48. 88
    0
      components/profile-content/ProfileContent.tsx
  49. 195
    0
      components/review-content/ReviewContent.tsx
  50. 128
    0
      components/shipping-content/ShippingContent.tsx
  51. 50
    0
      components/shipping-content/shipping-btnGroup/ButtonGroup.tsx
  52. 84
    0
      components/shipping-content/shipping-data/ShippingData.tsx
  53. 39
    0
      components/shipping-content/shipping-modal/ShippingModal.tsx
  54. 34
    0
      components/sort/Sort.tsx
  55. 91
    0
      components/tab-content/TabContent.tsx
  56. 27
    0
      components/tab-panel/TabPanel.tsx
  57. 12
    0
      constants/pages.ts
  58. 24
    0
      hooks/useCalculateTotal.ts
  59. 14
    0
      hooks/useFetchProductData.ts
  60. 24
    0
      hooks/useInfiniteQuery.ts
  61. 18
    5
      package.json
  62. 58
    3
      pages/_app.tsx
  63. 17
    0
      pages/_document.tsx
  64. 35
    0
      pages/auth/forgot-password/index.tsx
  65. 31
    0
      pages/auth/index.tsx
  66. 31
    0
      pages/auth/register/index.tsx
  67. 17
    0
      pages/cart/index.tsx
  68. 32
    0
      pages/checkout/index.tsx
  69. 18
    0
      pages/contact/index.tsx
  70. 57
    62
      pages/index.tsx
  71. 114
    0
      pages/products/[customId].tsx
  72. 17
    0
      pages/products/index.tsx
  73. 37
    0
      pages/profile/index.tsx
  74. 29
    0
      pages/review/index.tsx
  75. 31
    0
      pages/shipping/index.tsx
  76. 9
    0
      public/images/Facebook.svg
  77. 二進制
      public/images/Hero-Image.png
  78. 9
    0
      public/images/Instagram.svg
  79. 二進制
      public/images/Item 2.png
  80. 4
    0
      public/images/Play.svg
  81. 25
    0
      public/images/Stars.svg
  82. 9
    0
      public/images/Twitter.svg
  83. 3
    0
      public/images/arrow.svg
  84. 3
    0
      public/images/cart.svg
  85. 5
    0
      public/images/clock.svg
  86. 二進制
      public/images/coffee-bag 1.png
  87. 9
    0
      public/images/coffee-beans-icon.svg
  88. 9
    0
      public/images/coffee-beans.svg
  89. 9
    0
      public/images/coffee-machine.svg
  90. 9
    0
      public/images/coffee-mug.svg
  91. 9
    0
      public/images/factory.svg
  92. 二進制
      public/images/image-one.jpg
  93. 3
    0
      public/images/line.svg
  94. 3
    0
      public/images/lock.svg
  95. 57
    0
      public/images/logout.svg
  96. 9
    0
      public/images/mail.svg
  97. 9
    0
      public/images/maps.svg
  98. 9
    0
      public/images/pin.svg
  99. 二進制
      public/images/product-card-image.jpg
  100. 0
    0
      public/images/profile.svg

+ 51
- 0
components/buttons/load-more/LoadMore.tsx 查看文件

@@ -0,0 +1,51 @@
import { Button } from '@mui/material';
import CircularProgress from '@mui/material/CircularProgress';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';

const LoadMore = ({ fetchNextPage, isFetchingNextPage, hasNextPage }) => {
const { t } = useTranslation('products');
return (
<Button
onClick={fetchNextPage}
startIcon={
!isFetchingNextPage && (
<Image
src="/images/arrow.svg"
alt="arrow down"
width={29}
height={29}
/>
)
}
sx={{
backgroundColor: 'primary.main',
height: 50,
width: 150,
color: 'white',
':hover': {
bgcolor: 'primary.main',
color: 'white',
},
}}
>
{isFetchingNextPage && (
<CircularProgress
style={{
color: '#fff',
width: '29px',
height: '29px',
marginRight: '20px',
}}
/>
)}
{isFetchingNextPage
? t('products:loading')
: hasNextPage
? t('products:more')
: t('products:end')}
</Button>
);
};

export default LoadMore;

+ 24
- 0
components/cards/card-container/CardContainer.tsx 查看文件

@@ -0,0 +1,24 @@
import { Box } from '@mui/system';

const CardContainer = ({ children }) => {
return (
<Box
sx={{
ml: { md: 2 },
mt: { xs: 5, md: 0 },
display: 'flex',
flexDirection: {
xs: 'column',
sm: 'row',
lg: 'column',
},
justifyContent: { sm: 'flex-start' },
flexWrap: 'wrap',
}}
>
{children}
</Box>
);
};

export default CardContainer;

+ 176
- 0
components/cards/cart-card/CartCard.tsx 查看文件

@@ -0,0 +1,176 @@
import { Box, Button, ButtonGroup, Card, Typography } from '@mui/material';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
import { useState } from 'react';

const CartCard = ({ product, initialQuantity, remove, updateQuantity }) => {
const [quantity, setQuantity] = useState(initialQuantity);
const { t } = useTranslation('cart');
return (
<Card
sx={{
backgroundColor: '#f2f2f2',
p: 2,
mb: 2,
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
justifyContent: { xs: 'center' },
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
mb: { xs: 2, md: 0 },
}}
>
<Image src={product.image} alt="profile" width={200} height={200} />
</Box>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyItems: 'center',
width: { md: '40%' },
}}
>
<Typography
align="center"
sx={{
mb: { xs: 5, sm: 5, md: 0 },
mr: { md: 5 },
width: '100%',
fontWeight: 600,
fontSize: { xs: 20, sm: 20 },
}}
>
{product?.name}
</Typography>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'row', md: 'column' },
justifyContent: 'center',
alignItems: { xs: 'flex-end', md: 'center' },
mb: { xs: 5, sm: 5, md: 0 },
mr: { md: 5 },
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
mr: { xs: 2, md: 0 },
}}
>
<Typography
sx={{
width: '100%',
textAlign: 'center',
height: 16,
fontSize: 14,
}}
>
{t('cart:quantity')}
</Typography>
<ButtonGroup
size="small"
aria-label="small outlined button group"
sx={{
height: 35,
mt: 1,
backgroundColor: 'primary.main',
color: 'white',
border: 0,
}}
>
<Button
sx={{
color: 'white',
fontSize: 17,
width: 25,
}}
onClick={() => {
if (quantity > 1) {
updateQuantity(product?.customID, quantity - 1);
setQuantity((prevState) => prevState - 1);
}
}}
>
-
</Button>
<Button
sx={{
color: 'white',
fontSize: 15,
width: 25,
}}
>
{quantity}
</Button>
<Button
sx={{
color: 'white',
fontSize: 17,
width: 25,
}}
onClick={() => {
updateQuantity(product?.customID, quantity + 1);
setQuantity((prevState) => prevState + 1);
}}
>
+
</Button>
</ButtonGroup>
</Box>
<Button
disableRipple
sx={{
height: 35,
mt: 1,
width: 118,
fontSize: 15,
textTransform: 'none',
backgroundColor: '#C6453E',
color: 'white',
}}
startIcon={
<Image src="/images/x.svg" alt="remove" width={15} height={15} />
}
onClick={() => remove(product.customID)}
>
{t('cart:remove')}
</Button>
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Typography
sx={{
width: '100%',
textAlign: 'center',
height: 25,
fontSize: { xs: 15, md: 18 },
}}
>
{t('cart:priceTag')}
{product?.price}
</Typography>
</Box>
</Box>
</Card>
);
};

export default CartCard;

+ 69
- 0
components/cards/data-card/DataCard.tsx 查看文件

@@ -0,0 +1,69 @@
import { Box, Card, Typography } from '@mui/material';
import Image from 'next/image';

const DataCard = ({ data, quantity }) => {
return (
<Card
height="100%"
sx={{
backgroundColor: '#f2f2f2',
mb: 2,
p: 2,
mx: { xs: 0, sm: 1 },
width: { xs: '100%', sm: '44%', md: '100%', lg: '100%' },
}}
>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', lg: 'row' },
}}
>
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
<Image src={data.image} alt="profile" width={200} height={200} />
</Box>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyItems: 'center',
}}
>
<Typography
sx={{
textAlign: 'center',
fontWeight: 600,
fontSize: { md: 20, xs: 16 },
pt: { xs: 2 },
}}
>
{data.name}
</Typography>
<Typography
sx={{
width: '100%',
textAlign: 'center',
fontWeight: 600,
fontSize: { md: 20, xs: 16 },
}}
>
x{quantity}
</Typography>
<Typography
sx={{
mt: { lg: 3, xs: 1 },
textAlign: 'center',
fontSize: 14,
}}
>
${data.price} (per unit)
</Typography>
</Box>
</Box>
</Card>
);
};

export default DataCard;

+ 44
- 0
components/cards/order-card/OrderCard.tsx 查看文件

@@ -0,0 +1,44 @@
import { Card, Divider, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';

const OrderCard = ({ data }) => {
const { t } = useTranslation('profile');
return (
<Card
height="100%"
sx={{
backgroundColor: '#f2f2f2',
mb: 2,
p: 2,
mx: { xs: 0, sm: 1 },
width: { xs: '100%', sm: '47%', md: '100%', lg: '100%' },
}}
>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: { xs: 'center', md: 'flex-start' },
}}
>
<Typography sx={{ fontWeight: 600 }}>
{t('profile:orderDate')}
{data.date}
</Typography>
<Divider />
<Typography sx={{ mt: 1 }}>
{t('profile:by')}
{data.name}
</Typography>
<Typography>
{t('profile:total')}
{data.totalPrice.toFixed(2)}
</Typography>
</Box>
</Card>
);
};


export default OrderCard;

+ 73
- 0
components/cards/order-summary-card/OrderSummaryCard.tsx 查看文件

@@ -0,0 +1,73 @@
import { Button, Card, Divider, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { setCookie } from 'nookies';

const OrderSummaryCard = ({ data }) => {
const { t } = useTranslation('cart');
const router = useRouter();
return (
<Card sx={{ p: 3, width: '100%', mb: 2, backgroundColor: '#f1f1f1' }}>
<Typography
sx={{
fontSize: 26,
color: 'primary.main',
textAlign: 'center',
width: '100%',
}}
>
{t('cart:orderTitle')}
</Typography>
<Typography sx={{ mt: 4 }}>
{t('cart:itemsTotal')}
{data.totalPrice.toFixed(2)}
</Typography>
<Typography sx={{ mt: 1.5 }}>{t('cart:shipping')}</Typography>
<Typography sx={{ mt: 1.5, mb: 1.5 }}>
{t('cart:total')}
{data.totalPrice.toFixed(2)}
</Typography>
<Divider />
<Box sx={{ textAlign: 'center', mt: 4, width: '100%' }}>
<Button
disableRipple
sx={{
'&.Mui-disabled': {
backgroundColor: '#0066ff',
color: '#fff',
opacity: '0.6',
},
'&:hover': {
backgroundColor: '#0066ff',
color: 'white',
boxShadow: 'none',
},
backgroundColor: '#0066ff',
color: 'white',
textTransform: 'none',
px: 2,
}}
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: '/',
});
}}
>
{t('cart:proceed')}
</Button>
</Box>
<Typography sx={{ mt: 3, fontSize: 13 }}>{t('cart:infoMsg')}</Typography>
</Card>
);
};

export default OrderSummaryCard;

+ 74
- 0
components/cart-content/CartContent.tsx 查看文件

@@ -0,0 +1,74 @@
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import { destroyCookie } from 'nookies';
import { useEffect, useState } 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 EmptyCart from '../empty-cart/EmptyCart';
import ContentContainer from '../layout/content-wrapper/ContentContainer';
import PageWrapper from '../layout/page-wrapper/PageWrapper';
import StepTitle from '../layout/steps-title/StepTitle';

const CartContent = () => {
const { t } = useTranslation('cart');
const { cartStorage, totalPrice, totalQuantity } = useStore();
const { removeCartValue, updateItemQuantity } = useStoreUpdate();
const [cartInfo, setCartInfo] = useState({
cartStorage: [],
totalPrice: 0,
totalQuantity: 0,
});

useEffect(() => {
setCartInfo({
cartStorage,
totalPrice,
totalQuantity,
});
}, [cartStorage, totalPrice, totalQuantity]);

useEffect(() => {
destroyCookie(null, 'checkout-session', {
path: '/',
});
}, []);

const mapProductsToDom = () => {
if (cartInfo.cartStorage?.length) {
return cartInfo.cartStorage.map((element, i) => (
<CartCard
key={i}
product={element?.product}
initialQuantity={element?.quantity}
remove={removeCartValue}
updateQuantity={updateItemQuantity}
></CartCard>
));
} else {
return <EmptyCart />;
}
};

return (
<PageWrapper>
<StepTitle title={t('cart:cartTitle')} breadcrumbsArray={['Cart']} />
<ContentContainer>
<Box sx={{ mt: 2, mr: { md: 2, minWidth: '65%' }, mb: { xs: 6 } }}>
{mapProductsToDom()}
</Box>

<Box sx={{ mt: 2 }}>
<OrderSummaryCard
data={{
totalPrice: cartInfo.totalPrice,
totalQuantity: cartInfo.totalQuantity,
}}
></OrderSummaryCard>
</Box>
</ContentContainer>
</PageWrapper>
);
};

export default CartContent;

+ 76
- 0
components/checkout-content/CheckoutContent.tsx 查看文件

@@ -0,0 +1,76 @@
import { Box } from '@mui/system';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { setCookie } from 'nookies';
import { useEffect, useState } from 'react';
import { useStore } from '../../store/cart-context';
import { useCheckoutDataUpdate } from '../../store/checkout-context';
import CardContainer from '../cards/card-container/CardContainer';
import DataCard from '../cards/data-card/DataCard';
import ShippingDetailsForm from '../forms/shipping-details/ShippingDetailsForm';
import ContentContainer from '../layout/content-wrapper/ContentContainer';
import PageWrapper from '../layout/page-wrapper/PageWrapper';
import StepTitle from '../layout/steps-title/StepTitle';
import PageDescription from '../page-description/PageDescription';

const CheckoutContent = () => {
const { t } = useTranslation('cart');
const { cartStorage } = useStore();
const { addCheckoutValue } = useCheckoutDataUpdate();

const [cartData, setCartData] = useState([]);

const { data: session } = useSession();
const router = useRouter();

useEffect(() => {
setCartData(cartStorage);
}, [cartStorage]);

const submitHandler = (formValues) => {
addCheckoutValue(
cartData,
{ ...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 cartData?.map((entry, i) => (
<DataCard
key={i}
data={entry.product}
quantity={entry.quantity}
></DataCard>
));
};

return (
<PageWrapper>
<StepTitle
title={t('checkout:title')}
breadcrumbsArray={['Cart', 'Checkout']}
/>
<PageDescription description={t('checkout:subtitle')} />
<ContentContainer>
<Box flexGrow={1} sx={{ minWidth: '65%' }}>
<ShippingDetailsForm
backBtn={true}
isCheckout={true}
submitHandler={submitHandler}
></ShippingDetailsForm>
</Box>
<CardContainer>{mapProductsToDom()}</CardContainer>
</ContentContainer>
</PageWrapper>
);
};

export default CheckoutContent;

+ 120
- 0
components/company-info/CompanyInfo.tsx 查看文件

@@ -0,0 +1,120 @@
import { Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
const CompanyInfo = () => {
const { t } = useTranslation('home');
return (
<>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
backgroundColor: 'primary.main',
height: '100%',
paddingTop: '64px',
paddingBottom: '62px',
}}
>
<Box
sx={{
width: { xs: '100%', lg: '50%' },
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
paddingBottom: { xs: '60px', md: '0px' },
}}
>
<Typography
variant="h3"
sx={{
fontSize: { xs: '32px', md: '38px', lg: '48px' },
textAlign: 'center',
width: '100%',
color: 'white',
}}
>
{t('home:infoTitle')}
</Typography>
<Box
sx={{
mt: 3,
display: 'flex',
width: '100%',
justifyContent: 'center',
height: 60,
textAlign: 'center',
}}
>
<Image src="/images/pin.svg" alt="map" width={50} height={50} />
<Typography
sx={{
color: 'white',
pt: 2,
pl: 2,
}}
>
{t('home:address')}
</Typography>
</Box>
<Box
sx={{
mt: 3,
display: 'flex',
width: '100%',
justifyContent: 'center',
height: 60,
}}
>
<Image src="/images/clock.svg" alt="map" width={50} height={50} />
<Typography
sx={{
color: 'white',
pt: 2,
pl: 2,
mr: -4,
}}
>
{t('home:open')}
</Typography>
</Box>
<Box
sx={{
mt: 3,
display: 'flex',
width: '100%',
justifyContent: 'center',
height: 60,
}}
>
<Image src="/images/mail.svg" alt="map" width={50} height={50} />
<Typography
sx={{
color: 'white',
pt: 2,
pl: 2,
mr: -3,
}}
>
{t('home:mail')}
</Typography>
</Box>
</Box>
<Box
display="flex"
justifyContent="center"
alignItems="center"
sx={{ width: { xs: '100%', lg: '50%' } }}
>
<Box>
<Image src="/images/maps.svg" alt="map" width={1280} height={720} />
</Box>
</Box>
</Box>
</>
);
};

export default CompanyInfo;

+ 22
- 0
components/empty-cart/EmptyCart.tsx 查看文件

@@ -0,0 +1,22 @@
import { Typography } from '@mui/material';
import { useTranslation } from 'next-i18next';

const EmptyCart = () => {
const { t } = useTranslation('cart');
return (
<Typography
sx={{
mr: { lg: 1 },
mt: 6,
height: '100%',
textAlign: 'center',
fontSize: { xs: 36, md: 45 },
mb: { md: 5 },
}}
>
{t('cart:empty')}
</Typography>
);
};

export default EmptyCart;

+ 35
- 0
components/features/FeatureItem.tsx 查看文件

@@ -0,0 +1,35 @@
import { Container, Typography } from '@mui/material';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';

type FeatureItemProps = {
image: string;
alt: string;
description: string;
}

const FeatureItem: React.FC<FeatureItemProps> = ({ image, alt, description }) => {
const { t } = useTranslation('home');
return (
<Container
sx={{
textAlign: 'center',
display: 'flex',
flexDirection: 'column',
marginTop: { xs: '50px' },
}}
>
<Image src={image} alt={alt} width={100} height={100} />
<Typography
sx={{
mt: 6,
px: 6,
}}
>
{t(description)}
</Typography>
</Container>
);
};

export default FeatureItem;

+ 89
- 0
components/features/Features.tsx 查看文件

@@ -0,0 +1,89 @@
import { Container, Divider, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
import FeatureItem from './FeatureItem';
import items from './items';

const Features = () => {
const { t } = useTranslation('home');
return (
<>
<Box
sx={{
display: 'flex',
width: '100%',

height: {
xs: '100%',
sm: '100%',
},
flexDirection: 'column',
paddingBottom: '50px',
}}
>
<Container
sx={{
width: '100%',
}}
>
<Typography
variant="h1"
sx={{
fontSize: { xs: '36px', sm: '48px', md: '64px', lg: '86px' },
color: 'primary.main',
textAlign: 'center',
mt: 5,
fontFamily: ['Indie Flower', 'cursive'].join(','),
}}
>
{t('home:coffeeTitle')}
</Typography>
</Container>
<Container
sx={{
width: '100%',
textAlign: 'center',
}}
>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}}
>
<Divider sx={{ width: { xs: '100px', sm: '200px' }, mr: 4 }} />

<Image
src="/images/coffee-beans-icon.svg"
alt="profile"
width={50}
height={50}
/>
<Divider sx={{ width: { xs: '100px', sm: '200px' }, ml: 4 }} />
</Box>
</Container>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', lg: 'row' },
width: '100%',
height: '100%',
}}
>
{items.map((item) => (
<FeatureItem
key={item.id}
image={item.image}
alt={item.alt}
description={item.description}
/>
))}
</Box>
</Box>
</>
);
};

export default Features;

+ 23
- 0
components/features/items.ts 查看文件

@@ -0,0 +1,23 @@
const features = [
{
id: 1,
description: 'home:factory',
alt: 'image description',
image: '/images/factory.svg',
},
{
id: 2,
description: 'home:machine',
alt: 'image description',
image: '/images/coffee-machine.svg',
},
{
id: 3,
description: 'home:coffeeBeans',
alt: 'image description',
image: '/images/coffee-beans.svg',
},
];
export default features;

+ 29
- 0
components/filter-sort/FilterSort.tsx 查看文件

@@ -0,0 +1,29 @@
import { Box } from '@mui/system';
import ProductType from '../product-type/ProductType';
import Sort from '../sort/sort';

const FilterSort = ({
sort,
handleSortChange,
productType,
handleProductTypeChange,
}) => {
return (
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
justifyContent: { xs: 'center' },
alignItems: { xs: 'center' },
}}
>
<Sort sort={sort} handleSortChange={handleSortChange} />
<ProductType
productType={productType}
handleProductTypeChange={handleProductTypeChange}
/>
</Box>
);
};

export default FilterSort;

+ 75
- 0
components/forms/contact/ContactForm.tsx 查看文件

@@ -0,0 +1,75 @@
import { Box, Button, Paper, TextField } from '@mui/material';
import { useFormik } from 'formik';
import React, { useState } from 'react';
import { contactSchema } from '../../../schemas/contactSchema';
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
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="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}
fullWidth
/>
<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>
</Paper>
);
};

export default ContactForm;

+ 135
- 0
components/forms/contact/ContactPageForm.tsx 查看文件

@@ -0,0 +1,135 @@
import {
Box,
Button,
Container,
Grid,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import React, { useState } from 'react';
import { BASE_PAGE } from '../../../constants/pages';
import { postQuestion } from '../../../requests/question/postQuestionRequest';
import { contactPageSchema } from '../../../schemas/contactSchema';
import Notification from '../../notification/Notification';
const ContactPageForm = () => {
const { t } = useTranslation('contact');
const [open, setOpen] = useState(false);
const handleSubmit = async (values) => {
try {
postQuestion(values);
setOpen(true);
} catch (error) {
console.log(error);
}
};
const handleCloseNotification = () => {
setOpen(false);
};
const formik = useFormik({
initialValues: {
firstName: '',
lastName: '',
email: '',
message: '',
},
validationSchema: contactPageSchema,
onSubmit: handleSubmit,
validateOnBlur: true,
enableReinitialize: true,
});
return (
<Container component="main" maxWidth="md" sx={{ mb: '60px' }}>
<Notification
open={open}
notification={t('contact:notification')}
handleCloseNotification={handleCloseNotification}
/>
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography fontSize={48}>{t('contact:title')}</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="firstName"
label={t('contact: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('contact: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
name="email"
label={t('contact: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('contact: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}>
<Typography>{t('contact:back')}</Typography>
</Link>
</Grid>
</Box>
</Box>
</Container>
);
};
export default ContactPageForm;

+ 83
- 0
components/forms/forgot-password/ForgotPasswordForm.tsx 查看文件

@@ -0,0 +1,83 @@
import {
Box,
Button,
Container,
Grid,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import React from 'react';
import { LOGIN_PAGE } from '../../../constants/pages';
import { forgotPasswordSchema } from '../../../schemas/forgotPasswordSchema';

const ForgotPasswordForm = () => {
const { t } = useTranslation('forms', 'forgotPass', 'common');

const handleSubmit = (values) => {
console.log('Values', values);
};

const formik = useFormik({
initialValues: {
email: '',
},
validationSchema: forgotPasswordSchema,
onSubmit: handleSubmit,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
{t('forgotPass:Title')}
</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="email"
label={t('forms: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
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('forgotPass:SendBtn')}
</Button>
<Grid container justifyContent="center">
<Link href={LOGIN_PAGE}>
<Typography sx={{ cursor: 'pointer' }}>
{t('common:Back')}
</Typography>
</Link>
</Grid>
</Box>
</Box>
</Container>
);
};

export default ForgotPasswordForm;

+ 155
- 0
components/forms/login/LoginForm.tsx 查看文件

@@ -0,0 +1,155 @@
import {
Box,
Button,
Container,
Grid,
IconButton,
InputAdornment,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { signIn } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import {
BASE_PAGE,
FORGOT_PASSWORD_PAGE,
REGISTER_PAGE,
} from '../../../constants/pages';
import { loginSchema } from '../../../schemas/loginSchema';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

const LoginForm = () => {
const { t } = useTranslation('forms', 'login');
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const router = useRouter();
const [error, setError] = useState({ hasError: false, errorMessage: '' });

const submitHandler = async (values) => {
const result = await signIn('credentials', {
redirect: false,
username: values.username,
password: values.password,
});
if (!result.error) {
router.replace(BASE_PAGE);
} else {
setError({ hasError: true, errorMessage: result.error });
}
};

const formik = useFormik({
initialValues: {
username: '',
password: '',
},
validationSchema: loginSchema,
onSubmit: submitHandler,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
{t('login:Title')}
</Typography>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="username"
label={t('forms:Username')}
margin="normal"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
autoFocus
fullWidth
/>
<TextField
name="password"
label={t('forms:Password')}
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('login:LoginBtn')}
</Button>
<Grid container>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'left' }, mt: 1 }}
>
<Link href={FORGOT_PASSWORD_PAGE}>
<Typography sx={{ cursor: 'pointer' }}>
{t('login:ForgotPassword')}
</Typography>
</Link>
</Grid>
<Grid
item
xs={12}
md={6}
sx={{
textAlign: {
xs: 'center',
md: 'right',
},
mt: 1,
}}
>
<Link href={REGISTER_PAGE}>
<Typography sx={{ cursor: 'pointer' }}>
{t('login:NoAccount')}
</Typography>
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

export default LoginForm;

+ 263
- 0
components/forms/register/RegisterForm.tsx 查看文件

@@ -0,0 +1,263 @@
import {
Box,
Button,
Container,
Grid,
IconButton,
InputAdornment,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';

import { FORGOT_PASSWORD_PAGE, LOGIN_PAGE } from '../../../constants/pages';
import { createUser } from '../../../requests/accounts/accountRequests';
import { registerSchema } from '../../../schemas/registerSchema';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

const RegisterForm = () => {
const { t } = useTranslation('forms', 'register');
const router = useRouter();

const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => setShowPassword(!showPassword);
const handleMouseDownPassword = () => setShowPassword(!showPassword);

const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const handleClickShowConfirmPassword = () =>
setShowConfirmPassword(!showConfirmPassword);
const handleMouseDownConfirmPassword = () =>
setShowConfirmPassword(!showConfirmPassword);

const [error, setError] = useState({ hasError: false, errorMessage: '' });

const submitHandler = async (values) => {
try {
const result = await createUser(
values.fullName,
values.username,
values.email,
values.password,
values.address,
values.address2,
values.city,
values.country,
values.postcode
);
router.push(LOGIN_PAGE);
} catch (error) {
setError({ hasError: true, errorMessage: error.message });
}
};

const formik = useFormik({
initialValues: {
fullName: '',
username: '',
email: '',
password: '',
confirmPassword: '',
address: '',
address2: '',
city: '',
country: '',
postcode: '',
},
validationSchema: registerSchema,
onSubmit: submitHandler,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 10,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
{t('register:Title')}
</Typography>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="fullName"
label={t('forms:FullName')}
margin="normal"
value={formik.values.fullName}
onChange={formik.handleChange}
error={formik.touched.fullName && Boolean(formik.errors.fullName)}
helperText={formik.touched.fullName && formik.errors.fullName}
autoFocus
fullWidth
/>
<TextField
name="username"
label={t('forms:Username')}
margin="normal"
value={formik.values.username}
onChange={formik.handleChange}
error={formik.touched.username && Boolean(formik.errors.username)}
helperText={formik.touched.username && formik.errors.username}
fullWidth
/>
<TextField
name="email"
label={t('forms: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}
fullWidth
/>
<TextField
name="password"
label={t('forms:Password')}
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.password}
onChange={formik.handleChange}
error={formik.touched.password && Boolean(formik.errors.password)}
helperText={formik.touched.password && formik.errors.password}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowPassword}
onMouseDown={handleMouseDownPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<TextField
name="confirmPassword"
label={t('forms:ConfirmPassword')}
margin="normal"
type={showPassword ? 'text' : 'password'}
value={formik.values.confirmPassword}
onChange={formik.handleChange}
error={
formik.touched.confirmPassword &&
Boolean(formik.errors.confirmPassword)
}
helperText={
formik.touched.confirmPassword && formik.errors.confirmPassword
}
fullWidth
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleClickShowConfirmPassword}
onMouseDown={handleMouseDownConfirmPassword}
></IconButton>
</InputAdornment>
),
}}
/>
<TextField
name="address"
label="Address"
margin="normal"
value={formik.values.address}
onChange={formik.handleChange}
error={formik.touched.address && Boolean(formik.errors.address)}
helperText={formik.touched.address && formik.errors.address}
fullWidth
/>
<TextField
name="address"
label="Address2"
margin="normal"
value={formik.values.address2}
onChange={formik.handleChange}
error={formik.touched.address2 && Boolean(formik.errors.address2)}
helperText={formik.touched.address2 && formik.errors.address2}
fullWidth
/>
<TextField
name="city"
label="City"
margin="normal"
value={formik.values.city}
onChange={formik.handleChange}
error={formik.touched.city && Boolean(formik.errors.city)}
helperText={formik.touched.city && formik.errors.city}
fullWidth
/>
<TextField
name="country"
label="Country"
margin="normal"
value={formik.values.country}
onChange={formik.handleChange}
error={formik.touched.country && Boolean(formik.errors.country)}
helperText={formik.touched.country && formik.errors.country}
fullWidth
/>
<TextField
name="postcode"
label="Postal Code"
margin="normal"
value={formik.values.postcode}
onChange={formik.handleChange}
error={formik.touched.postcode && Boolean(formik.errors.postcode)}
helperText={formik.touched.postcode && formik.errors.postcode}
fullWidth
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('register:RegisterBtn')}
</Button>
<Grid container>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'left' }, mt: 1 }}
>
<Link href={FORGOT_PASSWORD_PAGE}>
<Typography sx={{ cursor: 'pointer' }}>
{t('register:ForgotPassword')}
</Typography>
</Link>
</Grid>
<Grid
item
xs={12}
md={6}
sx={{ textAlign: { xs: 'center', md: 'right' }, mt: 1 }}
>
<Link href={LOGIN_PAGE}>
<Typography sx={{ cursor: 'pointer' }}>
{t('register:HaveAccount')}
</Typography>
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};

export default RegisterForm;

+ 164
- 0
components/forms/shipping-details/ShippingDetailsForm.tsx 查看文件

@@ -0,0 +1,164 @@
import { Box, Button, Card, TextField } from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { useState } from 'react';
import { registerSchema } from '../../../schemas/shippingDetailsSchema';
import { useUserData } from '../../../store/user-context';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

const ShippingDetailsForm = ({
backBtn = false,
isCheckout = false,
submitHandler,
enableBtn = true,
}) => {
const { t } = useTranslation('addressForm');
const [error] = useState({ hasError: false, errorMessage: '' });
const { userStorage } = useUserData();
const router = useRouter();

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

const formik = useFormik({
initialValues: {
fullName: userStorage ? userStorage.fullName : '',
address: userStorage ? userStorage.address : '',
address2: userStorage ? userStorage.address2 : '',
city: userStorage ? userStorage.city : '',
country: userStorage ? userStorage.country : '',
postcode: userStorage ? userStorage.postcode : '',
},
validationSchema: registerSchema,
onSubmit: formikSubmitHandler,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Card sx={{ p: 3, backgroundColor: '#f2f2f2' }}>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="fullName"
label={t('addressForm:name')}
margin="normal"
value={formik.values.fullName}
onChange={formik.handleChange}
error={formik.touched.fullName && Boolean(formik.errors.fullName)}
helperText={formik.touched.fullName && formik.errors.fullName}
fullWidth
/>
<TextField
name="address"
label={t('addressForm:address')}
margin="normal"
value={formik.values.address}
onChange={formik.handleChange}
error={formik.touched.address && Boolean(formik.errors.address)}
helperText={formik.touched.address && formik.errors.address}
fullWidth
/>
<TextField
name="address2"
label={t('addressForm:address2')}
margin="normal"
value={formik.values.address2}
onChange={formik.handleChange}
error={formik.touched.address2 && Boolean(formik.errors.address2)}
helperText={formik.touched.address2 && formik.errors.address2}
fullWidth
/>
<TextField
name="city"
label={t('addressForm:city')}
margin="normal"
value={formik.values.city}
onChange={formik.handleChange}
error={formik.touched.city && Boolean(formik.errors.city)}
helperText={formik.touched.city && formik.errors.city}
fullWidth
/>
<Box sx={{ display: 'flex' }}>
<TextField
name="country"
label={t('addressForm:country')}
margin="normal"
value={formik.values.country}
onChange={formik.handleChange}
error={formik.touched.country && Boolean(formik.errors.country)}
helperText={formik.touched.country && formik.errors.country}
fullWidth
sx={{ mr: 1.5 }}
/>
<TextField
name="postcode"
label={t('addressForm:postcode')}
margin="normal"
value={formik.values.postcode}
onChange={formik.handleChange}
error={formik.touched.postcode && Boolean(formik.errors.postcode)}
helperText={formik.touched.postcode && formik.errors.postcode}
fullWidth
/>
</Box>

{backBtn && (
<Button
variant="contained"
sx={{
mt: 3,
mb: 2,
height: 50,
width: 150,
textTransform: 'none',
backgroundColor: 'primary.main',
color: 'white',
mr: 2,
}}
onClick={() => {
router.push('/cart');
}}
>
{t('addressForm:back')}
</Button>
)}
<Button
type="submit"
variant="contained"
sx={{
mt: 3,
mb: 2,
backgroundColor: '#CBA213',
height: 50,
width: isCheckout ? 200 : 150,
textTransform: 'none',
color: 'white',
}}
disabled={!enableBtn}
onClick={() => {
submitHandler;
}}
>
{isCheckout ? t('addressForm:shipping') : t('addressForm:submit')}
</Button>
</Box>
</Box>
</Card>
);
};

export default ShippingDetailsForm;

+ 11
- 0
components/grid-item/GridItem.tsx 查看文件

@@ -0,0 +1,11 @@
import { Grid } from '@mui/material';

const GridItem = ({ children }) => {
return (
<Grid item md={4} sm={6} xs={12} sx={{ mb: '100px' }}>
{children}
</Grid>
);
};

export default GridItem;

+ 142
- 0
components/hero/Hero.tsx 查看文件

@@ -0,0 +1,142 @@
import { Button, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
import { useRouter } from 'next/router';
import { PRODUCTS_PAGE } from '../../constants/pages';

const Hero = () => {
const { t } = useTranslation('home');

const router = useRouter();
return (
<>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
width: '100%',
height: { xs: '100vh', md: '1024px' },
}}
>
<Box
sx={{
minWidth: '50%',
width: { xs: '100%', md: '50%' },
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: { xs: 'space-around', md: 'center' },
backgroundColor: 'primary.light',
}}
>
<Box display="flex" flexDirection="column">
<Typography
variant="h1"
sx={{
fontSize: { xs: '96px', md: '64px', lg: '96px' },
ml: 10,
color: 'white',
fontFamily: ['Indie Flower', 'cursive'].join(','),
}}
>
{t('home:mainTitle1')}
</Typography>
<Typography
variant="h1"
sx={{
fontSize: { xs: '96px', md: '64px', lg: '96px' },
ml: 10,
color: 'white',
fontFamily: ['Indie Flower', 'cursive'].join(','),
}}
>
{t('home:mainTitle2')}
</Typography>
</Box>
<Typography
display="flex"
justifyItems="center"
sx={{
fontSize: { xs: '22px', md: '18px' },
ml: 10,
mt: { md: '50px' },
color: 'white',
pr: '20%',
}}
>
{t('home:description')}
</Typography>
<Box
sx={{
mt: { md: '50px' },
width: '100%',
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
ml: { md: 10 },
justifyContent: { sm: 'space-evenly', md: 'flex-start' },
alignItems: { xs: 'center' },
}}
>
<Button
disableRipple
sx={{
backgroundColor: '#CBA213',
mr: { md: 4 },
height: 50,
width: 150,
textTransform: 'none',

color: 'white',
}}
onClick={() => router.push(PRODUCTS_PAGE)}
>
{t('home:exploreBtn')}
</Button>
<Button
disableRipple
sx={{
display: { xs: 'none', sm: 'flex' },
textTransform: 'none',
color: 'white',
}}
startIcon={
<Image
src="/images/Play.svg"
alt="profile"
width={50}
height={50}
/>
}
>
{t('home:howTo')}
</Button>
</Box>
</Box>
<Box
sx={{
display: { xs: 'none', md: 'flex' },
backgroundColor: 'white',
}}
>
<Box
sx={{ ml: { md: -12 } }}
display="flex"
justifyContent="center"
alignItems="center"
>
<Image
src="/images/Hero-Image.png"
alt="profile"
width={818}
height={796}
priority
/>
</Box>
</Box>
</Box>
</>
);
};

export default Hero;

+ 23
- 0
components/layout/base-layout/Layout.tsx 查看文件

@@ -0,0 +1,23 @@
import { Box } from '@mui/material';
import React from 'react';
import Footer from '../footer/Footer';
import MainNav from '../navbar/MainNav';

type LayoutProps = {
children: JSX.Element | JSX.Element[]
}

const Layout: React.FC<LayoutProps> = ({ children }) => {
return (
<>
<Box sx={{ width: '100%' }}>
{/* <Navbar /> */}
<MainNav />
<main>{children}</main>
<Footer></Footer>
</Box>
</>
);
}

export default Layout;

+ 18
- 0
components/layout/content-wrapper/ContentContainer.tsx 查看文件

@@ -0,0 +1,18 @@
import { Box } from '@mui/system';

const ContentContainer = ({ children }) => {
return (
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
mr: { xs: 2, md: 12 },
ml: { xs: 2, md: 12 },
}}
>
{children}
</Box>
);
};

export default ContentContainer;

+ 148
- 0
components/layout/footer/Footer.tsx 查看文件

@@ -0,0 +1,148 @@
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Image from 'next/image';
import Link from 'next/link';
import { BASE_PAGE, PRODUCTS_PAGE } from '../../../constants/pages';

const pages = [
<Link key="home" href={BASE_PAGE}>
<Typography
textAlign="center"
sx={{
px: 1.5,
fontSize: 20,
fontWeight: 500,
color: 'black',
textDecoration: 'none',
cursor: 'pointer',
}}
>
Home
</Typography>
</Link>,
<Link key="menu" href={BASE_PAGE}>
<Typography
textAlign="center"
sx={{
px: 1.5,
fontSize: 20,
fontWeight: 500,
color: 'black',
textDecoration: 'none',
cursor: 'pointer',
}}
>
Menu
</Typography>
</Link>,
<Link key="about" href={BASE_PAGE}>
<Typography
textAlign="center"
sx={{
px: 1.5,
fontSize: 20,
fontWeight: 500,
color: 'black',
textDecoration: 'none',
cursor: 'pointer',
}}
>
About
</Typography>
</Link>,
<Link key="store" href={PRODUCTS_PAGE}>
<Typography
textAlign="center"
sx={{
px: 1.5,
fontSize: 20,
fontWeight: 500,
color: 'black',
textDecoration: 'none',
cursor: 'pointer',
}}
>
Store
</Typography>
</Link>,
<Link key="contact" href={BASE_PAGE}>
<Typography
textAlign="center"
sx={{
px: 1.5,
fontSize: 20,
fontWeight: 500,
color: 'black',
textDecoration: 'none',
cursor: 'pointer',
}}
>
Contact
</Typography>
</Link>,
];

const Footer: React.FC = () => {
return (
<Box
sx={{
display: 'flex',
width: '100%',
height: 220,
flexDirection: 'column',
bottom: 0,
position: 'relative',
}}
>
<Typography
variant="h3"
sx={{
width: '100%',
textAlign: 'center',
color: 'primary.main',
height: 60,
mt: 4,
}}
>
Coffee Shop
</Typography>
<Box
sx={{
maxWidth: '100%',
height: 30,
mt: 1.5,
display: 'flex',
justifyContent: 'center',
}}
>
{pages.map((page) => page)}
</Box>
<Box
sx={{
display: 'flex',
width: '100%',
height: 40,
mt: 4,
justifyContent: 'center',
}}
>
<Box sx={{ px: 2 }}>
<Image
src="/images/Instagram.svg"
alt="cart"
width={35}
height={35}
/>
</Box>
<Box sx={{ px: 2 }}>
<Image src="/images/Facebook.svg" alt="cart" width={35} height={35} />
</Box>
<Box sx={{ px: 2 }}>
<Image src="/images/Twitter.svg" alt="cart" width={35} height={35} />
</Box>
</Box>
</Box>
);
};

export default Footer;

+ 127
- 0
components/layout/navbar/DesktopNav.tsx 查看文件

@@ -0,0 +1,127 @@
import Box from '@mui/material/Box';
import Image from 'next/image';
import Link from 'next/link';
import React from 'react';
import { CART_PAGE, PROFILE_PAGE } from '../../../constants/pages';
import { NavItemDesktop } from './NavItem';
import { items } from './navItems';

interface DesktopNavProps {
router: any;
totalQuantity: number;
session: any;
signOutHandler: () => void;
}

const DesktopNav: React.FC<DesktopNavProps> = ({ router, totalQuantity, session, signOutHandler }) => {
return (
<Box sx={{ display: { xs: 'none', md: 'flex' }, width: '100%' }}>
<Box
sx={{
flexGrow: 1,
maxWidth: '50%',
height: 30,
display: 'flex',
justifyContent: 'center',
}}
>
{items.map((item) => (
<NavItemDesktop
key={item.id}
router={router}
name={item.name}
url={item.url}
/>
))}
</Box>
<Box
sx={{
flexGrow: 1,
maxWidth: '50%',
height: 30,
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,
cursor: 'pointer',
}}
>
<Link key="home" href={PROFILE_PAGE}>
<a>
<Image
src="/images/profile.svg"
alt="profile"
width={24}
height={24}
/>
</a>
</Link>
</Box>
<Box
sx={{
mr: 6,
ml: 2,
cursor: 'pointer',
}}
>
<Link key="home" href={CART_PAGE}>
<a>
<Box>
{totalQuantity !== 0 && (
<Box
sx={{
color: 'white',
zIndex: 3,
width: 20,
height: 20,
borderRadius: 20,
textAlign: 'center',
px: 0.5,
ml: 2.2,
mt: -1,
fontSize: 17,
position: 'absolute',
backgroundColor: 'primary.main',
}}
>
{totalQuantity}
</Box>
)}
<Image
src="/images/cart.svg"
alt="cart"
width={24}
height={24}
/>
</Box>
</a>
</Link>
</Box>
</Box>
</Box>
);
};

export default DesktopNav;

+ 105
- 0
components/layout/navbar/MainNav.tsx 查看文件

@@ -0,0 +1,105 @@
import MenuIcon from '@mui/icons-material/Menu';
import AppBar from '@mui/material/AppBar';
import IconButton from '@mui/material/IconButton';
import Toolbar from '@mui/material/Toolbar';
import useMediaQuery from '@mui/material/useMediaQuery';
import React, { useState } from 'react';

//drawer elements used
import { signOut, useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useStore } from '../../../store/cart-context';
import { useUserUpdate } from '../../../store/user-context';
import DesktopNav from './DesktopNav';
import MobileNav from './MobileNav';

export default function MainNav() {
//react useState hook to save the current open/close state of the drawer, normally variables dissapear afte the function was executed
const [open, setState] = useState(false);
const [cartQuantity, setCartQuantity] = useState(0);
const matches = useMediaQuery('(min-width: 900px)');

const router = useRouter();

const { data: session } = useSession();

const { totalQuantity } = useStore();

const { clearUser } = useUserUpdate();

const signOutHandler = async () => {
const data = await signOut({ redirect: false, callbackUrl: '/' });
clearUser();
router.push(data.url);
};

//function that is being called every time the drawer should open or close, the keys tab and shift are excluded so the user can focus between the elements with the keys
const toggleDrawer = (open) => (event) => {
if (
event.type === 'keydown' &&
(event.key === 'Tab' || event.key === 'Shift')
) {
return;
}
//changes the function state according to the value of open
setState(open);
};

useEffect(() => {
if (matches) {
setState(false);
}
}, [matches]);

useEffect(() => {
setCartQuantity(totalQuantity);
}, [totalQuantity]);

return (
<AppBar
position="absolute"
sx={{
zIndex: 100,
top: 20,
width: '100%',
backgroundColor: 'transparent',
boxShadow: 'none',
height: 40,
}}
>
<Toolbar sx={{ width: '100%' }}>
<DesktopNav
router={router}
totalQuantity={cartQuantity}
session={session}
signOutHandler={signOutHandler}
/>
<IconButton
edge="start"
color={router.pathname === '/' ? 'inherit' : 'primary'}
aria-label="open drawer"
onClick={toggleDrawer(true)}
sx={{
mr: 2,
display: {
xs: 'block',
md: 'none',
},
}}
>
<MenuIcon />
</IconButton>

{/* The outside of the drawer */}
<MobileNav
totalQuantity={totalQuantity}
session={session}
signOutHandler={signOutHandler}
toggleDrawer={toggleDrawer}
open={open}
/>
</Toolbar>
</AppBar>
);
}

+ 135
- 0
components/layout/navbar/MobileNav.tsx 查看文件

@@ -0,0 +1,135 @@
import AccountCircleIcon from '@mui/icons-material/AccountCircle';
import CloseIcon from '@mui/icons-material/Close';
import ShoppingCartIcon from '@mui/icons-material/ShoppingCart';
import { Box, Button, Divider, Drawer, IconButton } from '@mui/material';
import Image from 'next/image';
import Link from 'next/link';
import { CART_PAGE, PROFILE_PAGE } from '../../../constants/pages';
import { NavItemMobile } from './NavItem';
import { items } from './navItems';

interface MobileNavProps {
toggleDrawer: (toggle: boolean) => void;
session: any;
signOutHandler: () => void;
open: boolean;
totalQuantity?: number;

}

const MobileNav: React.FC<MobileNavProps> = ({
toggleDrawer,
session,
signOutHandler,
open,
totalQuantity,
}) => {
return (
<Drawer
PaperProps={{
sx: { width: { xs: '60%', sm: '50%' } },
}}
anchor="left"
open={open}
onClose={toggleDrawer.bind(null, false)}
>
<Box
sx={{
p: 2,
height: 1,
backgroundColor: '#fff',
}}
>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 2 }}>
<IconButton disableRipple onClick={toggleDrawer(false)}>
<CloseIcon color="primary" />
</IconButton>

{session?.user?._id && (
<IconButton disableRipple onClick={signOutHandler}>
<Image
src="/images/logout.svg"
alt="profile"
width={18}
height={20}
/>
</IconButton>
)}
</Box>

<Divider sx={{ mb: session?.user?._id ? 0 : 2 }} />

{session?.user?._id && (
<>
<Box display="flex" flexDirection="column" sx={{ ml: 1 }}>
<NavItemMobile
icon={<AccountCircleIcon sx={{ color: '#664c47' }} />}
toggleDrawer={toggleDrawer}
name="Profile"
url={PROFILE_PAGE}
/>
</Box>
<Divider sx={{ mb: 2 }} />
</>
)}

<Box sx={{ mb: 2, ml: 1 }} display="flex" flexDirection="column">
{items.map((item) => (
<NavItemMobile
key={item.id}
icon={item.icon}
toggleDrawer={toggleDrawer}
name={item.name}
url={item.url}
/>
))}

<Divider sx={{}} />
<NavItemMobile
totalQuantity={totalQuantity}
icon={<ShoppingCartIcon sx={{ color: '#664c47' }} />}
toggleDrawer={toggleDrawer}
name="Cart"
url={CART_PAGE}
/>
</Box>

<Box
sx={{
display: 'flex',
justifyContent: 'center',
position: 'absolute',
bottom: '0',
left: '50%',
transform: 'translate(-50%, 0)',
}}
>
{!session?.user?._id && (
<>
<Link href="/auth/register">
<Button
onClick={toggleDrawer.bind(null, false)}
variant="contained"
sx={{ m: 1, width: 0.5 }}
>
Register
</Button>
</Link>
<Link href="/auth">
<Button
onClick={toggleDrawer.bind(null, false)}
variant="outlined"
sx={{ m: 1, width: 0.5 }}
>
Login
</Button>
</Link>
</>
)}
</Box>
</Box>
</Drawer>
);
};

export default MobileNav;

+ 87
- 0
components/layout/navbar/NavItem.tsx 查看文件

@@ -0,0 +1,87 @@
import { Box, ListItemButton, ListItemText, Typography } from '@mui/material';
import Link from 'next/link';

type NavItemMobileProps = {
toggleDrawer: (toggle: boolean) => void;
icon: any;
name: string;
url: string;
totalQuantity: number;
}

export const NavItemMobile: React.FC<NavItemMobileProps> = ({
toggleDrawer,
icon,
name,
url,
totalQuantity,
}) => {
return (
<ListItemButton>
<Link href={url}>
<ListItemText
onClick={toggleDrawer.bind(null, false)}
primary={
<Box sx={{ display: 'flex' }}>
<Box sx={{ mt: 0.4, mr: 4 }}>{icon}</Box>
<Typography
sx={{ fontSize: '22px' }}
style={{ color: 'primary.main' }}
>
{name}
</Typography>
{name === 'Cart' && totalQuantity !== 0 && (
<Box
sx={{
color: 'white',
width: 20,
height: 20,
borderRadius: 20,
textAlign: 'center',
ml: 2.6,
mt: '-7px',
fontSize: 15,
position: 'absolute',
backgroundColor: 'primary.main',
}}
>
{totalQuantity}
</Box>
)}
</Box>
}
/>
</Link>
</ListItemButton>
);
};

type NavItemDesktopProps = {
url: string;
router: any;
name: string;
}

export const NavItemDesktop: React.FC<NavItemDesktopProps> = ({ url, router, name }) => {
return (
<Box sx={{ width: 150, mr: 3, ml: 3 }}>
<Link href={url}>
<Typography
textAlign="center"
sx={{
mx: 'auto',
width: '100%',
fontSize: { md: 24, lg: 24 },
mt: -1,
fontWeight: 500,
color: router.pathname === '/' ? 'white' : 'primary.main',
textDecoration: 'none',
cursor: 'pointer',
}}
>
{name}
</Typography>
</Link>
</Box>
);
};

+ 29
- 0
components/layout/navbar/navItems.tsx 查看文件

@@ -0,0 +1,29 @@
import ContactSupportIcon from '@mui/icons-material/ContactSupport';
import HomeIcon from '@mui/icons-material/Home';
import LocalMallIcon from '@mui/icons-material/LocalMall';
import {
BASE_PAGE,
CONTACT_PAGE,
PRODUCTS_PAGE,
} from '../../../constants/pages';

export const items = [
{
id: 1,
name: 'Home',
url: BASE_PAGE,
icon: <HomeIcon sx={{ color: '#664c47' }}></HomeIcon>,
},
{
id: 2,
name: 'Store',
url: PRODUCTS_PAGE,
icon: <LocalMallIcon sx={{ color: '#664c47' }}></LocalMallIcon>,
},
{
id: 3,
name: 'Contact',
url: CONTACT_PAGE,
icon: <ContactSupportIcon sx={{ color: '#664c47' }}></ContactSupportIcon>,
},
];

+ 7
- 0
components/layout/page-wrapper/PageWrapper.tsx 查看文件

@@ -0,0 +1,7 @@
import { Box } from '@mui/system';

const PageWrapper = ({ children }) => {
return <Box sx={{ py: 10, height: '100%', width: '100%' }}>{children}</Box>;
};

export default PageWrapper;

+ 55
- 0
components/layout/steps-title/StepTitle.tsx 查看文件

@@ -0,0 +1,55 @@
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
import { Breadcrumbs, Divider, Grid, Typography } from '@mui/material';

const StepTitle = ({ title, breadcrumbsArray }) => {
return (
<>
<Grid item xs={12}>
<Typography
variant="h4"
sx={{
ml: { xs: 2, md: 12 },
mt: 12,
height: '100%',
color: 'primary.main',
}}
>
{title}
</Typography>
</Grid>
<Grid item xs={12}>
<Divider
sx={{
backgroundColor: 'primary.main',
ml: { xs: 2, md: 12 },
mr: { xs: 2, md: 12 },
}}
/>
</Grid>
<Grid item xs={12} sx={{ mt: 4 }}>
<Breadcrumbs
aria-label="breadcrumb"
separator={<NavigateNextIcon fontSize="small" />}
sx={{ ml: { xs: 2, md: 12 }, fontSize: 20 }}
>
{breadcrumbsArray &&
breadcrumbsArray.map((entry, index) => {
return (
<Typography
sx={{ fontSize: { xs: '16px', md: '22px' } }}
key={index}
color={
index === breadcrumbsArray.length - 1 ? 'red' : 'black'
}
>
{entry}
</Typography>
);
})}
</Breadcrumbs>
</Grid>
</>
);
};

export default StepTitle;

+ 35
- 0
components/loader/Loader.tsx 查看文件

@@ -0,0 +1,35 @@
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';

const Loader = ({ loading }) => {
return (
loading && (
<Box
sx={{
display: 'flex',
zIndex: 99,
height: '100vh',
width: '100vw',
justifyContent: 'center',
alignItems: 'center',
position: 'fixed',
top: 0,
left: 0,
}}
>
<Box
sx={{
position: 'absolute',
top: '48%',
left: '48%',
marginX: 'auto',
}}
>
<CircularProgress color="inherit" size={60} thickness={4} />
</Box>
</Box>
)
);
};

export default Loader;

+ 11
- 0
components/loader/basic-spinner/LoadSpinner.tsx 查看文件

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

const LoadingSpinner = () => {
return (
<Box display="flex" justifyContent="center" sx={{ mt: 5 }}>
<CircularProgress />
</Box>
);
};

export default LoadingSpinner;

+ 56
- 0
components/loader/route-loader/CircularIndeterminate.tsx 查看文件

@@ -0,0 +1,56 @@
import Box from '@mui/material/Box';
import CircularProgress from '@mui/material/CircularProgress';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';

const CircularIndeterminate = () => {
const router = useRouter();

const [loading, setLoading] = useState(false);

useEffect(() => {
const handleStart = (url) => url !== router.asPath && setLoading(true);
const handleComplete = (url) => url === router.asPath && setLoading(false);

router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
router.events.on('routeChangeError', handleComplete);

return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleComplete);
router.events.off('routeChangeError', handleComplete);
};
});
return (
loading && (
<Box
sx={{
display: 'flex',
zIndex: 99,
height: '100vh',
width: '100vw',
justifyContent: 'center',
alignItems: 'center',
position: 'fixed',
top: 0,
left: 0,
}}
>
<Box
sx={{
position: 'absolute',
top: '48%',
left: '48%',
marginX: 'auto',
color: 'primary.dark',
}}
>
<CircularProgress color="inherit" size={60} thickness={4} />
</Box>
</Box>
)
);
};

export default CircularIndeterminate;

+ 15
- 0
components/mui/ErrorMessageComponent.tsx 查看文件

@@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Typography } from '@mui/material';

const ErrorMessageComponent = ({ error }) => (
<Typography variant="body1" color="error" my={2}>
{error}
</Typography>
);

ErrorMessageComponent.propTypes = {
error: PropTypes.string.isRequired,
};

export default ErrorMessageComponent;

+ 22
- 0
components/notification/Notification.tsx 查看文件

@@ -0,0 +1,22 @@
import { Alert, Snackbar } from '@mui/material';

const Notification = ({ handleCloseNotification, notification, open }) => {
return (
<Snackbar
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
open={open}
autoHideDuration={3000}
onClose={handleCloseNotification}
>
<Alert
onClose={handleCloseNotification}
severity="success"
sx={{ width: '100%', backgroundColor: 'green', color: 'white' }}
>
{notification}
</Alert>
</Snackbar>
);
};

export default Notification;

+ 12
- 0
components/page-description/PageDescription.tsx 查看文件

@@ -0,0 +1,12 @@
import { Typography } from '@mui/material';
import { Box } from '@mui/system';

const PageDescription = ({ description }) => {
return (
<Box sx={{ ml: { xs: 2, md: 12 }, my: 3 }}>
<Typography sx={{ fontSize: 20 }}>{description}</Typography>
</Box>
);
};

export default PageDescription;

+ 105
- 0
components/product-card/ProductCard.tsx 查看文件

@@ -0,0 +1,105 @@
import { Button, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
import NextLink from 'next/link';
import { useStore, useStoreUpdate } from '../../store/cart-context';

const ProductCard = ({ product }) => {
const { t } = useTranslation('products');
const { addCartValue } = useStoreUpdate();
const { cartStorage } = useStore();
const addProductToCart = (quantity) => addCartValue(product, quantity);
const inCart = cartStorage?.some(
(item) => item.product.customID === product.customID
)
? true
: false;
return (
<Box
sx={{
width: '100%',
height: '100%',
border: 'none',
mb: '15px',
backgroundColor: '#F5ECD4',
}}
>
<Box width="100%" sx={{ cursor: 'pointer' }}>
<NextLink
style={{ cursor: 'pointer' }}
href={`/products/${product.customID}`}
passHref
>
<a>
<Image
src={product.image}
alt="product image"
width={500}
height={390}
/>
</a>
</NextLink>
</Box>
<Box
width="100%"
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Typography
sx={{ height: '100px' }}
fontSize="24px"
align="center"
pt={1}
pb={3}
>
{product.name}
</Typography>
<Typography
sx={{
height: { xs: '200px', sm: '250px', md: '250px', lg: '200px' },
}}
align="center"
fontSize="18px"
m={2}
>
{product.description.length > 250
? product.description.slice(0, 250) + '...'
: product.description}
</Typography>
<Typography fontSize="24px" align="center" pt={4}>
${product.price}
</Typography>
<Box textAlign="center" mt={1}>
<Button
disableRipple
disableFocusRipple
disabled={inCart}
onClick={() => addProductToCart(1)}
sx={{
'&.Mui-disabled': {
backgroundColor: '#f2d675',
color: '#464646',
},
'&:hover': {
backgroundColor: '#f2d675',
color: '#464646',
boxShadow: 'none',
},
backgroundColor: '#CBA213',
height: 50,
width: 150,
color: 'white',
}}
>
{inCart ? t('products:in') : t('products:add')}
</Button>
</Box>
</Box>
</Box>
);
};

export default ProductCard;

+ 29
- 0
components/product-type/ProductType.tsx 查看文件

@@ -0,0 +1,29 @@
import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
import { useTranslation } from 'next-i18next';

const ProductType = ({ productType, handleProductTypeChange }) => {
const { t } = useTranslation('products');
return (
<>
<FormControl sx={{ width: '200px' }}>
<InputLabel id="product-type-label">{t('products:type')}</InputLabel>
<Select
MenuProps={{
disableScrollLock: true,
}}
label={t('products:type')}
labelId="product-type-label"
id="product-type-label"
value={productType}
onChange={handleProductTypeChange}
>
<MenuItem value="All">{t('products:all')}</MenuItem>
<MenuItem value="Coffee">{t('products:coffee')}</MenuItem>
<MenuItem value="Mug">{t('products:mug')}</MenuItem>
</Select>
</FormControl>
</>
);
};

export default ProductType;

+ 60
- 0
components/products-content/ProductsContent.tsx 查看文件

@@ -0,0 +1,60 @@
import { Box } from '@mui/system';
import Head from 'next/head';
import { useState } from 'react';
import { useInfiniteProducts } from '../../hooks/useInfiniteQuery';
import FilterSort from '../filter-sort/FilterSort';
import LoadingSpinner from '../loader/basic-spinner/LoadSpinner';
import ProductsGrid from '../products-grid/ProductsGrid';
import ProductsHero from '../products-hero/ProductsHero';

const ProductsContent = () => {
const [filter, setFilter] = useState('');
const [sort, setSort] = useState('');
const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } =
useInfiniteProducts(filter, sort);

const handleProductTypeChange = (event) => {
const filterText = event.target.value;
setFilter(filterText);
};

const handleSortChange = (event) => {
const sort = event.target.value;
setSort(sort);
};

return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Head>
<title>Coffee Shop</title>
<meta name="description" content="Random data with pagination..." />
</Head>
<ProductsHero />
<FilterSort
handleProductTypeChange={handleProductTypeChange}
productType={filter}
sort={sort}
handleSortChange={handleSortChange}
/>
{isLoading ? (
<LoadingSpinner />
) : (
<ProductsGrid
allProducts={data}
sort={sort}
productType={filter}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
/>
)}
</Box>
);
};

export default ProductsContent;

+ 43
- 0
components/products-grid/ProductsGrid.tsx 查看文件

@@ -0,0 +1,43 @@
import { Container, Grid } from '@mui/material';
import { Box } from '@mui/system';
import LoadMore from '../buttons/load-more/LoadMore';
import GridItem from '../grid-item/GridItem';
import ProductCard from '../product-card/ProductCard';

const ProductsGrid = ({
allProducts,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
}) => {
const dataToDisplay = allProducts.pages.map((page) =>
page.product.map((item) => (
<GridItem key={item._id}>
<ProductCard product={item} />
</GridItem>
))
);

return (
<Container
sx={{
mt: 10,
}}
>
<Grid container spacing={2}>
{dataToDisplay}
</Grid>
<Box textAlign="center" mt={-5} mb={5}>
{hasNextPage && (
<LoadMore
fetchNextPage={fetchNextPage}
isFetchingNextPage={isFetchingNextPage}
hasNextPage={hasNextPage}
/>
)}
</Box>
</Container>
);
};

export default ProductsGrid;

+ 43
- 0
components/products-hero/ProductsHero.tsx 查看文件

@@ -0,0 +1,43 @@
import { Container, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';

const ProductsHero = () => {
const { t } = useTranslation('products');
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Container
maxWidth="lg"
sx={{
mt: 25,
mb: 10,
}}
>
<Typography
fontFamily={'body1.fontFamily'}
align="center"
color="primary.main"
mb={3}
sx={{
fontSize: { md: '64px', sm: '46px', xs: '32px' },
}}
>
{t('products:title')}
</Typography>
<Typography
sx={{ fontSize: { xs: '16px', sm: '18px', md: '24px' } }}
align="center"
>
{t('products:description')}
</Typography>
</Container>
</Box>
);
};

export default ProductsHero;

+ 78
- 0
components/products/featured-product/FeaturedProduct.tsx 查看文件

@@ -0,0 +1,78 @@
import { Container } from '@mui/material';
import useMediaQuery from '@mui/material/useMediaQuery';
import { Box } from '@mui/system';
import { useEffect, useState } from 'react';
import { useStore, useStoreUpdate } from '../../../store/cart-context';
import ProductImage from './ProductImage';
import ProductInfo from './ProductInfo';

const FeaturedProduct = ({ product, bColor, side }) => {
const matches = useMediaQuery('(min-width: 900px)');
const data = { name: product.name, description: product.description };
const { addCartValue } = useStoreUpdate();
const { cartStorage } = useStore();
const addProductToCart = (quantity) => addCartValue(product, quantity);
const [inCart, setInCart] = useState(false);

useEffect(() => {
if (cartStorage) {
if (
cartStorage?.some((item) => item.product.customID === product.customID)
)
setInCart(true);
}
}, [cartStorage, product.customID]);

return (
<Box
sx={{
width: '100%',
backgroundColor: bColor === 'dark' ? 'primary.main' : 'primary.light',
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
padding: '30px 0 30px 0',
alignItems: { md: 'center' },
}}
>
<Container
maxWidth="xl"
sx={{ display: { md: 'flex' }, alignItems: { md: 'center' } }}
>
{side === 'left' ? (
<ProductImage image={product.image}></ProductImage>
) : !matches ? (
<ProductImage image={product.image}></ProductImage>
) : (
<ProductInfo
bColor={bColor}
side={side}
data={data}
addProductToCart={addProductToCart}
inCart={inCart}
></ProductInfo>
)}
{side === 'left' ? (
<ProductInfo
bColor={bColor}
side={side}
data={data}
addProductToCart={addProductToCart}
inCart={inCart}
></ProductInfo>
) : !matches ? (
<ProductInfo
bColor={bColor}
side={side}
data={data}
addProductToCart={addProductToCart}
inCart={inCart}
></ProductInfo>
) : (
<ProductImage image={product.image}></ProductImage>
)}
</Container>
</Box>
);
};

export default FeaturedProduct;

+ 19
- 0
components/products/featured-product/ProductImage.tsx 查看文件

@@ -0,0 +1,19 @@
import { Box } from '@mui/system';
import Image from 'next/image';

const ProductImage = ({ image }) => {
return (
<Box
sx={{
display: 'flex',
width: { xs: '100%', md: '50%' },
height: '100%',
justifyContent: { xs: 'center' },
}}
>
<Image src={image} alt="profile" width={500} height={500} />
</Box>
);
};

export default ProductImage;

+ 154
- 0
components/products/featured-product/ProductInfo.tsx 查看文件

@@ -0,0 +1,154 @@
import { Button, ButtonGroup, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import Image from 'next/image';
import { useState } from 'react';

const ProductInfo = ({ data, bColor, addProductToCart, inCart }) => {
const { t } = useTranslation('home');
const [quantity, setQuantity] = useState(1);

const handleIncrement = () => {
setQuantity((prevState) => prevState + 1);
};

const handleDecrement = () => {
if (quantity > 1) {
setQuantity((prevState) => prevState - 1);
}
};

return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: { xs: 'center', md: 'flex-start' },
width: { xs: '100%', md: '50%' },
height: '100%',
}}
>
<Typography variant="h3" sx={{ mt: { xs: 5 }, color: 'white' }}>
{data.name}
</Typography>
<Box
sx={{
display: 'flex',
alignItems: { xs: 'center', md: 'flex-start' },
justifyContent: { xs: 'center', md: 'flex-start' },
width: '100%',
py: { xs: 2 },
}}
>
<Image
src="/images/Stars.svg"
alt="reviews"
width={100}
height={50}
></Image>
</Box>
<Typography
sx={{
color: 'white',
}}
>
{data.description}
</Typography>
<Box
sx={{
width: '100%',
display: 'flex',
mt: 6,
flexDirection: { md: 'row' },
alignItems: { xs: 'center' },
justifyContent: { xs: 'center', md: 'flex-start' },
}}
>
<ButtonGroup
disabled={inCart}
size="small"
aria-label="small outlined button group"
sx={{
height: 50,
backgroundColor: bColor === 'light' ? '#664c47' : '#8f7772',
color: 'white',
border: 0,
}}
>
<Button
disableRipple
sx={{
'&.Mui-disabled': {
color: 'rgba(255, 255, 255, 0.6)',
},
color: 'white',
fontSize: 20,
width: 50,
}}
onClick={() => {
handleDecrement();
}}
>
-
</Button>
<Button
disableRipple
sx={{
'&.Mui-disabled': {
color: 'rgba(255, 255, 255, 0.6)',
},
color: 'white',
fontSize: 17,
width: 50,
}}
>
{quantity}
</Button>
<Button
disableRipple
sx={{
'&.Mui-disabled': {
color: 'rgba(255, 255, 255, 0.6)',
},
color: 'white',
fontSize: 20,
width: 50,
}}
onClick={() => {
handleIncrement();
}}
>
+
</Button>
</ButtonGroup>
<Button
disableRipple
sx={{
mt: { md: 0 },
ml: { xs: 2 },
backgroundColor: '#CBA213',
height: 50,
width: 150,
color: 'white',
'&.Mui-disabled': {
backgroundColor: '#f2d675',
color: '#464646',
},
'&:hover': {
backgroundColor: '#f2d675',
color: '#464646',
boxShadow: 'none',
},
}}
disabled={inCart}
onClick={() => addProductToCart(quantity)}
>
{inCart ? t('home:in') : t('home:add')}
</Button>
</Box>
</Box>
);
};


export default ProductInfo;

+ 27
- 0
components/products/featured-products-list/FeaturedPorductsList.tsx 查看文件

@@ -0,0 +1,27 @@
import { Box } from '@mui/system';
import FeaturedProduct from '../featured-product/FeaturedProduct';

const FeaturedProductsList = ({ featuredProducts }) => {
return (
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
{featuredProducts.map((product, i) => {
return (
<FeaturedProduct
key={i}
product={product}
bColor={i % 2 === 0 ? 'dark' : 'light'}
side={i % 2 === 0 ? 'left' : 'right'}
></FeaturedProduct>
);
})}
</Box>
);
};

export default FeaturedProductsList;

+ 88
- 0
components/profile-content/ProfileContent.tsx 查看文件

@@ -0,0 +1,88 @@
import { Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import { updateUser } from '../../requests/user/userUpdateRequest';
import { useUserUpdate } from '../../store/user-context';
import CardContainer from '../cards/card-container/CardContainer';
import OrderCard from '../cards/order-card/OrderCard';
import ShippingDetailsForm from '../forms/shipping-details/ShippingDetailsForm';
import ContentContainer from '../layout/content-wrapper/ContentContainer';
import PageWrapper from '../layout/page-wrapper/PageWrapper';
import StepTitle from '../layout/steps-title/StepTitle';
import Notification from '../notification/Notification';

const ProfileContent = ({ orders }) => {
const { t } = useTranslation('profile');
const { data: session } = useSession();
const { updateUserInfo } = useUserUpdate();
const [enableBtn, setEnableBtn] = useState(true);
const [open, setOpen] = useState(false);

const updateUserHandler = async (values) => {
try {
setEnableBtn(false);
updateUserInfo(values);
await updateUser(values, session.user._id);
setOpen(true);
setTimeout(() => {
setEnableBtn(true);
}, 5000);
} catch (error) {
console.log(error);
setTimeout(() => {
setEnableBtn(true);
}, 3000);
}
};

const handleCloseNotification = () => {
setOpen(false);
};

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 (
<PageWrapper>
<StepTitle title={t('profile:title')} />
<Notification
open={open}
handleCloseNotification={handleCloseNotification}
notification={t('profile:notification')}
/>

<ContentContainer>
<Box flexGrow={1} sx={{ minWidth: '65%' }}>
<Typography sx={{ fontSize: 20, mb: 3 }}>
{t('profile:subtitle1')}
</Typography>
<ShippingDetailsForm
submitHandler={updateUserHandler}
enableBtn={enableBtn}
></ShippingDetailsForm>
</Box>
<Box sx={{ mt: { xs: 5, md: 0 } }}>
<Typography
sx={{ fontSize: 20, mb: { xs: -2, md: 3 }, ml: { md: 3 } }}
>
{t('profile:subtitle2')}
</Typography>
<CardContainer>{mapOrdersToDom()}</CardContainer>
</Box>
</ContentContainer>
</PageWrapper>
);
};

export default ProfileContent;

+ 195
- 0
components/review-content/ReviewContent.tsx 查看文件

@@ -0,0 +1,195 @@
import { Button, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
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 {
useCheckoutData,
useCheckoutDataUpdate,
} from '../../store/checkout-context';
import PageWrapper from '../layout/page-wrapper/PageWrapper';
import StepTitle from '../layout/steps-title/StepTitle';

let initialRender = true;

const ReviewContent = () => {
const { t } = useTranslation('review');
const { checkoutStorage } = useCheckoutData();
const { parseCheckoutValue, clearCheckout } = useCheckoutDataUpdate();
const { clearCart } = useStoreUpdate();
const [orderData, setOrderData] = useState({});

const router = useRouter();

useEffect(() => {
if (initialRender) {
setOrderData(parseCheckoutValue());
postOrder(parseCheckoutValue());
initialRender = false;
return () => {
clearCheckout();
clearCart();
destroyCookie(null, 'checkout-session', {
path: '/',
});
destroyCookie(null, 'shipping-session', {
path: '/',
});
destroyCookie(null, 'review-session', {
path: '/',
});
};
}
}, [checkoutStorage]);

return (
<PageWrapper>
<StepTitle
title="Review"
breadcrumbsArray={['Cart', 'Checkout', 'Shipping', 'Payment', 'Review']}
/>
<Box sx={{ ml: { xs: 2 }, mr: { xs: 2 }, mt: 6 }}>
<Box>
<Typography
sx={{
width: '100%',
textAlign: 'center',
color: 'primary.main',
fontWeight: 600,
fontSize: 22,
}}
>
{t('review:orderMsg')}
</Typography>
</Box>
<Box sx={{ mt: 1 }}>
<Typography
sx={{
width: '100%',
fontWeight: 600,
mt: 2,
textAlign: 'center',
}}
>
{t('review:note')}
</Typography>
</Box>
<Box sx={{ mt: 1 }}>
<Typography
sx={{
width: '100%',
textAlign: 'center',
mt: 4,
mb: 4,
fontSize: 44,
fontWeight: 600,
}}
>
{t('review:title')}
</Typography>
</Box>
<Box
sx={{
backgroundColor: '#f2f2f2',
my: 1,
ml: { md: 12 },
mr: { md: 12 },
borderRadius: 2,
p: 2,
}}
>
<Typography sx={{ fontSize: 18, fontWeight: 600 }}>
{t('review:date')}
{orderData.time}
</Typography>
</Box>
<Box
sx={{
backgroundColor: '#f2f2f2',
ml: { md: 12 },
mr: { md: 12 },
borderRadius: 2,
p: 2,
my: 1,
}}
>
<Typography sx={{ fontSize: 18, fontWeight: 600 }}>
{t('review:email')}
{orderData?.shippingAddress?.email}
</Typography>
</Box>
<Box
sx={{
backgroundColor: '#f2f2f2',
ml: { md: 12 },
mr: { md: 12 },
borderRadius: 2,
p: 2,
my: 1,
}}
>
<Typography sx={{ fontSize: 18, fontWeight: 600 }}>
{t('review:total')}
{orderData?.totalPrice?.toFixed(2)}
</Typography>
</Box>
<Box
sx={{
backgroundColor: '#f2f2f2',
ml: { md: 12 },
mr: { md: 12 },
borderRadius: 2,
p: 2,
my: 1,
}}
>
<Typography sx={{ fontSize: 18, fontWeight: 600 }}>
{t('review:shipping')}
{orderData?.shippingAddress?.address},{' '}
{orderData?.shippingAddress?.city},{' '}
{orderData?.shippingAddress?.country},{' '}
{orderData?.shippingAddress?.postcode}
</Typography>
</Box>
<Box sx={{ mt: 1 }}>
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
mt: 2,
borderRadius: 2,
p: 1,
}}
>
<Button
variant="contained"
sx={{
mt: 3,
mb: 2,
height: 50,
width: 150,
textTransform: 'none',
backgroundColor: '#CBA213',
color: 'white',
mr: 2,
fontSize: 16,
}}
onClick={() => {
router.push('/');
}}
>
{t('review:back')}
</Button>
</Box>
</Box>
</Box>
</PageWrapper>
);
};

export default ReviewContent;

+ 128
- 0
components/shipping-content/ShippingContent.tsx 查看文件

@@ -0,0 +1,128 @@
import { Checkbox, FormControlLabel } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { setCookie } from 'nookies';
import { useEffect, useState } from 'react';
import {
useCheckoutData,
useCheckoutDataUpdate,
} from '../../store/checkout-context';
import { stripe } from '../../utils/helpers/stripe';
import CardContainer from '../cards/card-container/CardContainer';
import DataCard from '../cards/data-card/DataCard';
import ContentContainer from '../layout/content-wrapper/ContentContainer';
import PageWrapper from '../layout/page-wrapper/PageWrapper';
import StepTitle from '../layout/steps-title/StepTitle';
import PageDescription from '../page-description/PageDescription';
import ButtonGroup from './shipping-btnGroup/ButtonGroup';
import ShippingData from './shipping-data/ShippingData';
import ShippingModal from './shipping-modal/ShippingModal';

const ShippingContent = () => {
const { t } = useTranslation('shipping');
const { checkoutStorage } = useCheckoutData();
const { changeContact, changeShippingData } = useCheckoutDataUpdate();
const [open, setOpen] = useState({ isOpen: false, type: '' });
const [checkoutData, setCheckoutData] = useState({});

const router = useRouter();

useEffect(() => {
setCheckoutData(checkoutStorage);
}, [checkoutStorage]);

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: checkoutData.products.map((el) => ({
price: el.product.stripeID,
quantity: el.quantity,
})),
userInfo: checkoutData.userInfo,
});
setCookie(null, 'review-session', 'active', {
maxAge: 3600,
expires: new Date(Date.now() + 3600),
path: '/',
});
};

const handleBackToCart = () => {
router.replace('/cart');
};

const mapProductsToDom = () => {
return checkoutData?.products?.map((entry, i) => (
<DataCard
key={i}
data={entry.product}
quantity={entry.quantity}
></DataCard>
));
};

return (
<PageWrapper>
<StepTitle
title={t('shipping:title')}
breadcrumbsArray={['Cart', 'Checkout', 'Shipping']}
/>
<PageDescription description={t('shipping:subtitle')} />
<ContentContainer>
<Box flexGrow={1} sx={{ minWidth: '65%' }}>
<ShippingData
email={checkoutData?.userInfo?.email}
address={checkoutData?.userInfo?.address}
city={checkoutData?.userInfo?.city}
postcode={checkoutData?.userInfo?.postcode}
handleOpen={handleOpen}
/>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
backgroundColor: '#f2f2f2',
alignItems: 'center',
mb: 2,
width: { sm: '200px' },
borderRadius: 2,
p: 1,
}}
>
<FormControlLabel
control={<Checkbox checked disabled />}
label={t('shipping:shippingCost')}
sx={{ color: 'black', ml: 2 }}
/>
</Box>
<ButtonGroup
handleStripePayment={handleStripePayment}
handleBackToCart={handleBackToCart}
/>
</Box>
<CardContainer>{mapProductsToDom()}</CardContainer>
</ContentContainer>
<ShippingModal
open={open}
handleClose={handleClose}
handleChangeShipping={handleChangeShipping}
handleChangeContact={handleChangeContact}
/>
</PageWrapper>
);
};

export default ShippingContent;

+ 50
- 0
components/shipping-content/shipping-btnGroup/ButtonGroup.tsx 查看文件

@@ -0,0 +1,50 @@
import { Box, Button } from '@mui/material';
import { useTranslation } from 'next-i18next';

const ButtonGroup = ({ handleBackToCart, handleStripePayment }) => {
const { t } = useTranslation('shipping');
return (
<Box
sx={{
display: 'flex',
mb: 2,
borderRadius: 2,
}}
>
<Button
variant="contained"
sx={{
mt: 3,
mb: 2,
height: 50,
width: 150,
textTransform: 'none',
backgroundColor: 'primary.main',
color: 'white',
mr: 2,
}}
onClick={handleBackToCart}
>
{t('shipping:back')}
</Button>
<Button
type="submit"
variant="contained"
sx={{
mt: 3,
mb: 2,
backgroundColor: '#CBA213',
height: 50,
width: 200,
textTransform: 'none',
color: 'white',
}}
onClick={handleStripePayment}
>
{t('shipping:continue')}
</Button>
</Box>
);
};

export default ButtonGroup;

+ 84
- 0
components/shipping-content/shipping-data/ShippingData.tsx 查看文件

@@ -0,0 +1,84 @@
import { Button, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';

const ShippingData = ({ email, address, city, postcode, handleOpen }) => {
const { t } = useTranslation('shipping');

return (
<>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
backgroundColor: '#f2f2f2',
alignItems: 'center',
mb: 2,
borderRadius: 2,
p: 1,
}}
>
<Typography sx={{ fontSize: 18, fontWeight: 600 }}>
{t('shipping: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');
}}
>
{t('shipping:changeBtn')}
</Button>
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
backgroundColor: '#f2f2f2',
alignItems: 'center',
mb: 2,
borderRadius: 2,
p: 1,
}}
>
<Typography
sx={{
fontSize: { md: 18, xs: 16 },
fontWeight: 600,
mr: { xs: 1, sm: 0 },
}}
>
{t('shipping:shipping')}
</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');
}}
>
{t('shipping:changeBtn')}
</Button>
</Box>
</>
);
};

export default ShippingData;

+ 39
- 0
components/shipping-content/shipping-modal/ShippingModal.tsx 查看文件

@@ -0,0 +1,39 @@
import { Modal } from '@mui/material';
import { Box } from '@mui/system';
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: { xs: '90%', md: '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>
);
};

export default ShippingModal;

+ 34
- 0
components/sort/Sort.tsx 查看文件

@@ -0,0 +1,34 @@
import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
import { useTranslation } from 'next-i18next';

const Sort = ({ sort, handleSortChange }) => {
const { t } = useTranslation('products');
return (
<>
<FormControl
sx={{
width: '200px',
mb: { xs: '10px', sm: '0px' },
mr: { sm: '10px' },
}}
>
<InputLabel id="sort-label">{t('products:sort')}</InputLabel>
<Select
MenuProps={{
disableScrollLock: true,
}}
label={t('products:sort')}
labelId="sort-label"
id="sort-select-helper"
value={sort}
onChange={handleSortChange}
>
<MenuItem value="asc">{t('products:asc')}</MenuItem>
<MenuItem value="desc">{t('products:desc')}</MenuItem>
</Select>
</FormControl>
</>
);
};

export default Sort;

+ 91
- 0
components/tab-content/TabContent.tsx 查看文件

@@ -0,0 +1,91 @@
import { Button, Grid, Tab, Tabs, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import { useState } from 'react';
import TabPanel from '../tab-panel/TabPanel';

const TabContent = ({
description,
inCart,
price,
category,
addProductToCart,
}) => {
const [value, setValue] = useState(0);
const { t } = useTranslation('products');

const handleChange = (event, newValue) => {
setValue(newValue);
};

function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}

return (
<Grid item xs={12} md={6}>
<Tabs
sx={{
'& button:focus': {
borderTop: '1px solid black',
borderLeft: '1px solid black',
borderRight: '1px solid black',
borderRadius: '5px 5px 0 0',
borderBottom: 'none',
},
}}
value={value}
onChange={handleChange}
aria-label="basic tabs example"
>
<Tab
sx={{
width: '50%',
}}
label={t('products:purchase')}
{...a11yProps(0)}
/>
<Tab
sx={{ width: '50%' }}
label={t('products:category')}
{...a11yProps(1)}
/>
</Tabs>
<TabPanel value={value} index={0}>
<Box flexGrow={2} sx={{ pb: { xs: '70px' } }}>
<Typography>{description}</Typography>
</Box>
<Box
sx={{
display: { xs: 'flex' },
flexDirection: { xs: 'column' },
justifyContent: { xs: 'center' },
alignItems: { xs: 'center', md: 'flex-end' },
}}
>
<Typography mb={2}>${price}</Typography>
<Button
disabled={inCart}
onClick={() => addProductToCart(1)}
sx={{
backgroundColor: '#CBA213',
height: 50,
width: { xs: '300px', md: '150px' },
color: 'white',
}}
>
{inCart ? t('products:in') : t('products:add')}
</Button>
</Box>
</TabPanel>
<TabPanel value={value} index={1}>
<Box sx={{ mb: { xs: '60px' } }}>{category}</Box>
</TabPanel>{' '}
</Grid>
);
};

export default TabContent;

+ 27
- 0
components/tab-panel/TabPanel.tsx 查看文件

@@ -0,0 +1,27 @@
import { Box } from '@mui/system';

const TabPanel = ({ children, value, index, ...other }) => {
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
style={{ height: '80%' }}
>
{value === index && (
<Box
display="flex"
flexDirection="column"
alignContent="space-between"
sx={{ pt: 3, pl: 3, width: '100%', height: '100%' }}
>
{children}
</Box>
)}
</div>
);
};

export default TabPanel;

+ 12
- 0
constants/pages.ts 查看文件

@@ -0,0 +1,12 @@
export const BASE_PAGE: string = '/';
export const CHECKOUT_PAGE: string = '/checkout';
export const CART_PAGE: string = '/cart';
export const SHIPPING_PAGE: string = '/shipping';
export const REVIEW_PAGE: string = '/review';
export const PRODUCTS_PAGE: string = '/products';
export const LOGIN_PAGE: string = '/auth';
export const PROFILE_PAGE: string = '/profile';
export const REGISTER_PAGE: string = '/auth/register';
export const FORGOT_PASSWORD_PAGE: string = '/auth/forgot-password';
export const SINGLE_DATA_PAGE: string = '/single-data/';
export const CONTACT_PAGE: string = '/contact';

+ 24
- 0
hooks/useCalculateTotal.ts 查看文件

@@ -0,0 +1,24 @@
import { useState } from 'react';
import { getStorage } from '../utils/helpers/storage';

const useCalculateTotal = () => {
const CART_KEY = 'cart-products';

const [total, setTotal] = useState(() => {
const cart = getStorage(CART_KEY);

if (cart && cart.length) {
return cart
.map((entry) => entry?.product.price * entry?.quantity)
.reduce((accum, curValue) => accum + curValue);
} else {
return 0;
}
});

return {
total,
};
};

export default useCalculateTotal;

+ 14
- 0
hooks/useFetchProductData.ts 查看文件

@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/react-query';
import { getProductData } from '../requests/products/producDataRequest';

export const useFetchSingleProduct = (customID: string) => {
return useQuery(
['product', customID],
async () => await getProductData(customID),
{
refetchOnWindowFocus: false,
staleTime: 60000,
cacheTime: 300000,
}
);
};

+ 24
- 0
hooks/useInfiniteQuery.ts 查看文件

@@ -0,0 +1,24 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { getAllProducts } from '../requests/products/productRequest';

export const useInfiniteProducts = (category: string, filter: string) => {
return useInfiniteQuery(
['products', category, filter],
async ({ pageParam = 1 }) =>
await getAllProducts(
pageParam,
category === '' ? 'All' : category,
filter === '' ? 'asc' : filter
),
{
getNextPageParam: (lastPage, pages) => {
if (lastPage.next !== null) {
return pages.length + 1;
}
},
refetchOnWindowFocus: false,
staleTime: 60000,
cacheTime: 300000,
}
);
};

+ 18
- 5
package.json 查看文件

@@ -9,19 +9,32 @@
"lint": "next lint"
},
"dependencies": {
"@sendgrid/mail": "^7.7.0",
"@types/bcryptjs": "^2.4.2",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@mui/codemod": "^5.10.8",
"@mui/icons-material": "^5.10.6",
"@mui/material": "^5.10.8",
"@stripe/stripe-js": "^1.39.0",
"@tanstack/react-query": "^4.10.3",
"@types/mongodb": "^4.0.7",
"@types/validator": "^13.7.7",
"bcryptjs": "^2.4.3",
"mongoose": "^6.6.5",
"formik": "^2.2.9",
"next": "12.3.1",
"next-auth": "^4.13.0",
"next-i18next": "^11.3.0",
"nookies": "^2.5.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-i18next": "^11.18.6",
"yup": "^0.32.11",
"@sendgrid/mail": "^7.7.0",
"@types/bcryptjs": "^2.4.2",
"@types/validator": "^13.7.7",
"bcryptjs": "^2.4.3",
"mongoose": "^6.6.5",
"validator": "^13.7.0"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^4.11.0",
"@types/node": "18.8.3",
"@types/react": "18.0.21",
"@types/react-dom": "18.0.6",

+ 58
- 3
pages/_app.tsx 查看文件

@@ -1,8 +1,63 @@

import Head from 'next/head';
import { ThemeProvider } from '@mui/material/styles';
import theme from '../styles/muiTheme';
import {
Hydrate,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { Session } from "next-auth";
import { SessionProvider } from 'next-auth/react';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { appWithTranslation } from 'next-i18next';
import StorageProvider from '../store/cart-context';
import CheckoutProvider from '../store/checkout-context';
import UserProvider from '../store/user-context';
import Layout from '../components/layout/base-layout/Layout';
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { useState } from 'react';
import type { DehydratedState } from '@tanstack/react-query';

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

function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
function MyApp({ Component, pageProps }: AppProps<{ dehydratedState: DehydratedState, session: Session }>) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<SessionProvider session={pageProps.session}>
<ThemeProvider theme={theme}>
<Providers
components={[CheckoutProvider, StorageProvider, UserProvider]}
>
<Layout>
<Head>
<title>Coffee Shop</title>
<meta name="description" content="NextJS template" />
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
</Head>
<Component {...pageProps} />
</Layout>
</Providers>
</ThemeProvider>
</SessionProvider>
<ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools>
</Hydrate>
</QueryClientProvider>)
}

export default MyApp
export default appWithTranslation<never>(MyApp);

+ 17
- 0
pages/_document.tsx 查看文件

@@ -0,0 +1,17 @@
import Document, { Head, Html, Main, NextScript } from 'next/document';

class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

export default MyDocument;

+ 35
- 0
pages/auth/forgot-password/index.tsx 查看文件

@@ -0,0 +1,35 @@
import { NextPage } from 'next';
import { getSession } from 'next-auth/react';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import ForgotPasswordForm from '../../../components/forms/forgot-password/ForgotPasswordForm';
import { BASE_PAGE } from '../../../constants/pages';

const ForgotPasswordPage: NextPage = () => {
const router = useRouter();

useEffect(() => {
getSession().then((session) => {
if (session) {
router.replace(BASE_PAGE);
}
});
}, [router]);

return <ForgotPasswordForm />;
};

export async function getStaticProps({ locale }: any) {
return {
props: {
...(await serverSideTranslations(locale, [
'forms',
'forgotPass',
'common',
])),
},
};
}

export default ForgotPasswordPage;

+ 31
- 0
pages/auth/index.tsx 查看文件

@@ -0,0 +1,31 @@
import { NextPage } from 'next';
import { getSession } from 'next-auth/react';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import LoginForm from '../../components/forms/login/LoginForm';
import { BASE_PAGE } from '../../constants/pages';

const AuthPage: NextPage = () => {
const router = useRouter();

useEffect(() => {
getSession().then((session) => {
if (session) {
router.replace(BASE_PAGE);
}
});
}, [router]);

return <LoginForm />;
};

export async function getStaticProps({ locale }: any) {
return {
props: {
...(await serverSideTranslations(locale, ['forms', 'login'])),
},
};
}

export default AuthPage;

+ 31
- 0
pages/auth/register/index.tsx 查看文件

@@ -0,0 +1,31 @@
import { NextPage } from 'next';
import { getSession } from 'next-auth/react';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import RegisterForm from '../../../components/forms/register/RegisterForm';
import { BASE_PAGE } from '../../../constants/pages';

const RegisterPage: NextPage = () => {
const router = useRouter();

useEffect(() => {
getSession().then((session) => {
if (session) {
router.replace(BASE_PAGE);
}
});
}, [router]);

return <RegisterForm />;
};

export async function getStaticProps({ locale }: any) {
return {
props: {
...(await serverSideTranslations(locale, ['forms', 'register'])),
},
};
}

export default RegisterPage;

+ 17
- 0
pages/cart/index.tsx 查看文件

@@ -0,0 +1,17 @@
import { NextPage } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import CartContent from '../../components/cart-content/CartContent';

const CartPage: NextPage = () => {
return <CartContent></CartContent>;
};

export async function getStaticProps({ locale }: any) {
return {
props: {
...(await serverSideTranslations(locale, ['cart'])),
},
};
}

export default CartPage;

+ 32
- 0
pages/checkout/index.tsx 查看文件

@@ -0,0 +1,32 @@
import { NextPage } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import nookies from 'nookies';
import CheckoutContent from '../../components/checkout-content/CheckoutContent';

const CheckoutPage: NextPage = () => {
return <CheckoutContent></CheckoutContent>;
};

export const getServerSideProps = async (ctx: any) => {
const cookies = nookies.get(ctx);

if (!cookies['checkout-session']) {
return {
redirect: {
destination: '/cart',
permanent: false,
},
};
}

return {
props: {
...(await serverSideTranslations(ctx.locale, [
'checkout',
'addressForm',
])),
},
};
};

export default CheckoutPage;

+ 18
- 0
pages/contact/index.tsx 查看文件

@@ -0,0 +1,18 @@
import type { NextPage } from 'next';

import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import ContactPageForm from '../../components/forms/contact/ContactPageForm';

const Contact: NextPage = () => {
return <ContactPageForm />;
};

export const getStaticProps = async ({ locale }: any) => {
return {
props: {
...(await serverSideTranslations(locale, ['contact'])),
},
};
}

export default Contact;

+ 57
- 62
pages/index.tsx 查看文件

@@ -1,72 +1,67 @@
import type { NextPage } from 'next'
import { useSession } from 'next-auth/react';
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import { Box } from '@mui/system';
import Hero from '../components/hero/Hero';
import { getFeaturedProducts } from '../requests/products/featuredProductsRequest';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import FeaturedProductsList from '../components/products/featured-products-list/FeaturedPorductsList';
import Features from '../components/features/Features';
import CompanyInfo from '../components/company-info/CompanyInfo';
import { FeaturedProductsResponse } from '../requests/products/featuredProductsRequest';
import { useUserUpdate } from '../store/user-context';
import { getStorage } from '../utils/helpers/storage';
import { useEffect } from 'react';

const Home: NextPage = () => {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>

<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>

<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.tsx</code>
</p>

<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation &rarr;</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>

<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn &rarr;</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
const Home: NextPage<FeaturedProductsResponse> = ({ featuredProducts }) => {
const { data: session } = useSession();
const { addUser } = useUserUpdate();

<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples &rarr;</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
useEffect(() => {
const userData = getStorage('user-data');
if (session?.user && userData.length === 0) {
addUser(session.user);
}
}, [session, addUser]);

<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy &rarr;</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>

<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
return (
<>
<Box sx={{ width: '100%', height: '100%' }}>
<Head>
<title>Coffee Shop</title>
<meta name="description" content="Random data with pagination..." />
</Head>
<Hero />
<FeaturedProductsList
featuredProducts={featuredProducts}
></FeaturedProductsList>
<Features />
<CompanyInfo />
</Box>
</>
)
}

export async function getStaticProps({ locale }: any) {
try {
const { message, featuredProducts } = await getFeaturedProducts();
return {
props: {
...(await serverSideTranslations(locale, ["home"])),
message,
featuredProducts,
},
};
} catch (error) {
return {
props: {
...(await serverSideTranslations(locale, ['home'])),
errorMessage: error,
featuredProducts: [],
},
};
}
}

export default Home

+ 114
- 0
pages/products/[customId].tsx 查看文件

@@ -0,0 +1,114 @@
import { Grid, Typography } from '@mui/material';
import { Box, Container } from '@mui/system';
import { dehydrate, QueryClient } from '@tanstack/react-query';
import { NextPage } from 'next';
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import Image from 'next/image';
import { useRouter } from 'next/router';
import React from 'react';
import GridItem from '../../components/grid-item/GridItem';
import Loader from '../../components/loader/Loader';
import ProductCard from '../../components/product-card/ProductCard';
import TabContent from '../../components/tab-content/TabContent';
import { useFetchSingleProduct } from '../../hooks/useFetchProductData';
import { getProductData } from '../../requests/products/producDataRequest';
import { useStore, useStoreUpdate } from '../../store/cart-context';

const SingleProduct: NextPage = () => {
const { t } = useTranslation('products');
const { addCartValue } = useStoreUpdate();
const { cartStorage } = useStore();

const router = useRouter();

const { customId } = router.query;

const { data, isLoading } = useFetchSingleProduct(customId);

const addProductToCart = (quantity) => addCartValue(data.product, quantity);
const inCart = cartStorage?.some(
(item) => item.product.customID === data?.product.customID
)
? true
: false;

if (isLoading) {
return <Loader loading={isLoading} />;
}

return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Container>
<Typography
fontFamily={'body1.fontFamily'}
fontSize="32px"
sx={{ mt: 25, height: '100%', color: 'primary.main' }}
>
{data.product.name}
</Typography>
<Grid container spacing={2}>
<Grid sx={{ display: 'flex' }} item md={6} sm={12}>
<Image
src={data.product.image}
alt="product"
width={900}
height={700}
/>
</Grid>
<TabContent
description={data?.product.description}
inCart={inCart}
price={data?.product.price}
category={data?.product.category}
addProductToCart={addProductToCart}
/>
</Grid>

<Typography
sx={{
mt: { xs: '60px', md: '100px', lg: '150px' },
mb: 5,
color: 'primary.main',
fontSize: '32px',
}}
>
{t('products:similar')}
</Typography>
<Grid container spacing={2}>
{data.similarProducts.map((product) => (
<GridItem key={product._id}>
<ProductCard product={product} />
</GridItem>
))}
</Grid>
</Container>
</Box>
);
};

export const getServerSideProps = async (context: any) => {
const { params } = context;
const { customId } = params;

const queryClient = new QueryClient();

await queryClient.prefetchQuery(
['product', customId],
async () => await getProductData(customId)
);

return {
props: {
dehydratatedState: dehydrate(queryClient),
...(await serverSideTranslations(context.locale, ['products'])),
},
};
};

export default SingleProduct;

+ 17
- 0
pages/products/index.tsx 查看文件

@@ -0,0 +1,17 @@
import { NextPage } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import ProductsContent from '../../components/products-content/ProductsContent';

const Products: NextPage = () => {
return <ProductsContent></ProductsContent>;
};

export async function getStaticProps({ locale }: any) {
return {
props: {
...(await serverSideTranslations(locale, ['products'])),
},
};
}

export default Products;

+ 37
- 0
pages/profile/index.tsx 查看文件

@@ -0,0 +1,37 @@
import { NextPage } from 'next';
import { getSession } from 'next-auth/react';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import ProfileContent from '../../components/profile-content/ProfileContent';
import { LOGIN_PAGE } from '../../constants/pages';
import { getOrdersForOwner } from '../../requests/orders/getOrdersForOwnerRequest';

const ProfilePage: NextPage = (props) => {
return <ProfileContent orders={props.orders.orders}></ProfileContent>;
};

export async function getServerSideProps(context) {
const session = await getSession({ req: context.req });

if (!session) {
return {
redirect: {
destination: LOGIN_PAGE,
permanent: false,
},
};
}

const orders = await getOrdersForOwner(session.user._id);

return {
props: {
...(await serverSideTranslations(context.locale, [
'profile',
'addressForm',
])),
orders,
},
};
}

export default ProfilePage;

+ 29
- 0
pages/review/index.tsx 查看文件

@@ -0,0 +1,29 @@
import { NextPage } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import nookies from 'nookies';
import ReviewContent from '../../components/review-content/ReviewContent';

const ReviewPage: NextPage = () => {
return <ReviewContent></ReviewContent>;
};

export const getServerSideProps = async (ctx: any) => {
const cookies = nookies.get(ctx);

if (!cookies['review-session']) {
return {
redirect: {
destination: '/cart',
permanent: false,
},
};
}

return {
props: {
...(await serverSideTranslations(ctx.locale, ['review'])),
},
};
};

export default ReviewPage;

+ 31
- 0
pages/shipping/index.tsx 查看文件

@@ -0,0 +1,31 @@
import { NextPage } from 'next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
import nookies from 'nookies';
import ShippingContent from '../../components/shipping-content/ShippingContent';

const ShippingPage: NextPage = () => {
return <ShippingContent></ShippingContent>;
};

export const getServerSideProps = async (ctx: any) => {
const cookies = nookies.get(ctx);

if (!cookies['shipping-session']) {
return {
redirect: {
destination: '/cart',
permanent: false,
},
};
}

return {
props: {
...(await serverSideTranslations(ctx.locale, [
'shipping',
'addressForm',
])),
},
};
};
export default ShippingPage;

+ 9
- 0
public/images/Facebook.svg
文件差異過大導致無法顯示
查看文件


二進制
public/images/Hero-Image.png 查看文件


+ 9
- 0
public/images/Instagram.svg
文件差異過大導致無法顯示
查看文件


二進制
public/images/Item 2.png 查看文件


+ 4
- 0
public/images/Play.svg 查看文件

@@ -0,0 +1,4 @@
<svg width="72" height="72" viewBox="0 0 72 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="36" cy="36" r="36" fill="#FFFAF5"/>
<path d="M50 36L29 48.1244L29 23.8756L50 36Z" fill="#CBA213"/>
</svg>

+ 25
- 0
public/images/Stars.svg
文件差異過大導致無法顯示
查看文件


+ 9
- 0
public/images/Twitter.svg
文件差異過大導致無法顯示
查看文件


+ 3
- 0
public/images/arrow.svg 查看文件

@@ -0,0 +1,3 @@
<svg width="29" height="29" viewBox="0 0 29 29" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 12L15 18L9 12" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

+ 3
- 0
public/images/cart.svg 查看文件

@@ -0,0 +1,3 @@
<svg width="18" height="24" viewBox="0 0 18 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.74999 7.08527V5.38527C4.74999 2.95767 6.64549 0.977173 8.99999 0.977173C11.3545 0.977173 13.25 2.95767 13.25 5.38527V7.08527H16.1876C16.3222 7.08537 16.4517 7.13646 16.5501 7.22825C16.6485 7.32005 16.7085 7.44572 16.718 7.57997L17.7278 22.455C17.7367 22.5955 17.6897 22.7339 17.597 22.8399C17.5043 22.9459 17.3735 23.011 17.2331 23.0211H0.802586C0.661915 23.0211 0.527006 22.9652 0.427537 22.8657C0.328067 22.7663 0.272186 22.6313 0.272186 22.4907V22.455L1.28199 7.57997C1.29103 7.44542 1.35083 7.31932 1.44929 7.22718C1.54775 7.13503 1.67753 7.0837 1.81239 7.08357L4.74999 7.08527ZM6.34459 7.08527H11.6571V5.38527C11.6571 3.82297 10.4671 2.57007 8.99999 2.57007C7.53289 2.57007 6.34459 3.82297 6.34459 5.38527V7.08527ZM1.93989 21.4299H16.0601L15.1965 8.67987H2.80519L1.93989 21.4299Z" fill="#664C47"/>
</svg>

+ 5
- 0
public/images/clock.svg 查看文件

@@ -0,0 +1,5 @@
<svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="25" cy="25" r="24.5" stroke="white"/>
<line x1="24.5" y1="7" x2="24.5" y2="25" stroke="white"/>
<line x1="25.1936" y1="25.0256" x2="11.7117" y2="31.6011" stroke="white"/>
</svg>

二進制
public/images/coffee-bag 1.png 查看文件


+ 9
- 0
public/images/coffee-beans-icon.svg
文件差異過大導致無法顯示
查看文件


+ 9
- 0
public/images/coffee-beans.svg
文件差異過大導致無法顯示
查看文件


+ 9
- 0
public/images/coffee-machine.svg
文件差異過大導致無法顯示
查看文件


+ 9
- 0
public/images/coffee-mug.svg
文件差異過大導致無法顯示
查看文件


+ 9
- 0
public/images/factory.svg
文件差異過大導致無法顯示
查看文件


二進制
public/images/image-one.jpg 查看文件


+ 3
- 0
public/images/line.svg 查看文件

@@ -0,0 +1,3 @@
<svg width="120" height="3" viewBox="0 0 120 3" fill="none" xmlns="http://www.w3.org/2000/svg">
<line y1="1.5" x2="120" y2="1.5" stroke="#8F7772" stroke-opacity="0.7" stroke-width="3"/>
</svg>

+ 3
- 0
public/images/lock.svg 查看文件

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.8043 6.49485V3.80412C12.8043 1.70651 11.0978 0 9.00021 0C6.90259 0 5.19608 1.70651 5.19608 3.80412V6.49485H2.59814V18H15.4023V6.49485H12.8043ZM6.30948 3.80412C6.30948 2.32044 7.51652 1.1134 9.00021 1.1134C10.4839 1.1134 11.6909 2.32044 11.6909 3.80412V6.49485H6.30948V3.80412ZM14.2889 16.8866H3.71155V7.60825H14.2889V16.8866Z" fill="white"/>
</svg>

+ 57
- 0
public/images/logout.svg 查看文件

@@ -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 fill='#664C47' 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 fill='#664C47' 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>

+ 9
- 0
public/images/mail.svg 查看文件

@@ -0,0 +1,9 @@
<svg width="70" height="66" viewBox="0 0 70 66" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="70" height="66" transform="matrix(-1 0 0 1 70 0)" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_64_24" transform="translate(0 -0.030303) scale(0.00390625 0.00414299)"/>
</pattern>
<image id="image0_64_24" width="256" height="256" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAHFgAABxYB45pq/gAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAA0pSURBVHic7d17zJxlmYDx6ynlUFFcWRBXUaELDVQIgYpSJFWEVDFVIREQK6y1Wre4cnBZNbtuYqJslsPSFUHYosJSVFIhIIcURBCtSlGWhLBhiYcQ4hpkYXU5iLWA9/7xvBNETv3a75l3Zu7rl/APlLmfb+adq/PNvO8zJQgk5TSj7wVI6o8BkBIzAFJiBkBKzABIiRkAKTEDICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMQMgJSYAZASMwBSYgZASswASIkZACkxAyAlZgCkxAyAlJgBkBIzAFJiBkBKzABIiRkAKTEDICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMQMgJSYAZASMwBSYgZASswASIkZACkxAyAlZgCkxAyAlJgBkBIzAFJiBkBKzABIiRkAKTEDICVmAKTEDICUmAGQEpvZ9wIIdgU+2fcypB6cRuGePhdQguhzPgQHALf0uwipF/MprOtzAf4KICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMRGIQCPAo/3vQhpyB6nHvu96j8Ahf8EdgfOB37f82qk1n5PPdZ37479XvV/MdAfC14FfAL4MDCr59VI0+l3wAXA6RR+2fdiBkYrAAPBTsApwHJg255XI22O3wLnAWdSuL/vxfyp0QzAQLADcDLwN8B2Pa9GmoqHgXOAFRQe7Hsxz2W0AzAQvAw4ATgReFnPq5Gez2+AzwNnU/hN34t5IeMRgIFgO+qrgZOBHXpejfTHHgRWAOdQeLjvxWys8QrAQLAt9f2BU4Cdel6NcrsfOBM4j8Jv+17MVI1nAAaCWdRPDD4BvKrn1SiXXwKnAxdQ+F3fi9lU4x2AgWBrYAnwKeC1Pa9Gk+1e4J+BCynjf97KZARgINgSOBb4e+Ave16NJsvPgX8CVlEm58zVyQrAQLAF8D5qCPboeTUab3dTn/hfo/Bk34uZbpMZgIFgBnAk8A/A3j2vRuPlTuBU4BsU/tD3YlqZ7AAMBAU4HPg0sF/Pq9Foux34HHAlZfKfHP1fDBS8muDNTWcUgsIVFOYBi4Bbm87TOLoVWERhXnestH3yB28meHXTGRuh/wDAy4GvE1xDcGjzaYVrKRwALATWNp+nUbcWWEjhAArXNp8WHEpwDfB16rHfq1EIwMB+wMUE3yI4rHvZ3k7hBgoLgIOBm5rO0ii6CTiYwgIKNzSdFJTumP4WcDEj9GvoKAVgYC/gy8CNBO/q3shrp3AzhUOANwHXNZ2lUXAd8CYKh1C4uemkYAbBu4Abqcf0Xk3nbYJRDMDAHtSNE24meE/30V47hR9SOAzYH7gKJv8NoESC+pjuT+EwCj9sPG0LgvcAN1OP4ZH9KHqUAzCwG3A2sJbgmO5kn3YKt1F4N7AvcBmGYJwF9THcl8K7KdzWeNqWBMdQ31c4m3rsjrRxCMDALsC/AD8gOI5gq6bTCndQOJL6su1rMHkngUywJ6mP2V4UjqRwR9NpwVYExwE/oB6juzSdN43GKQADO1PPxV5HsJRgm6bTCndRWAzMBf4deKLpPG2OJ6iP0VwKiync1XRasA3BUmAd9Zjcuem8BsYxAAOvAD4L3EqwnOBFTacVfkLhA8Ac6t5uG5rO01RsoD4mcyh8gMJPmk4LXkSwnHruwGepx+JYGucADOwI/CPwI4ITCF7SdFrhHgrLqL/fnQusbzpPz2c99THYjcIyCvc0nRa8hOAE4EfUY27HpvOGoP9TgYN5wNXTeIsPAV8CvkThoWm83WcX/AXwd8BHoPGrEA08BvwbcAaF+5pPC14KfKj756XTeMvvpPAf03h7UzaJARh4BLgQWEnh1w1u/+mClwMfBz4KvLj5vJwepf6NfxaF/2k+LdgeWEbda6LFK0sD0DAAA49R3xg6n8IDDedU9aA5GfgY0/u3RWYPAV+g7rA7jJjvCPw18Fe0fVVnAIYQgIH1wFeBcyn8qvm0+rLxBOAkYPvm8ybTr4F/pe6wO4xf515BfQW3GBp/ulQZgCEGYGADcCl199b/bj6tvil5PPC3TMCbRkPyAPXz9C9SeKT5tGBn6m7T74XG55c8Xe8BmIRPAaZqK+hO2gjOIhqftFF4hMJp1JNDPg5DeNNqfN1HvY92oXBa8yd/sAvBWdQTeI5juE/+kZAxAANbUou/luBsovFpm4XHKKwAZlPfH/hF03nj5RfU+2Q2hRUUHms6LdiN6E4vr8dA29PLR1jmAAxsAd2FG8H5ROMLNwrrKZxDPY/gI9D4s+vRdg/1PtiNwjmUxudUBHsQ3QVm9TFve4HZGDAAT5kB3aWbwZeJxnsIFjZQWEk9s3AJ8NOm80bLT6k/8xwKKymNz6oM9ia6S8zrY+xx3/GOeKYCHAZcT3Ax0XjzhsITFC4C9qS++9z2/PV+3UX9GfekcBGl8XUVwX4EFwPXUx/TtpvMjCED8PwOBa4huJTgDU0nFZ6kdFew1Z2M217BNlx3QHdlZRnC9trBGwguBa6BIWwzN8YMwMZZAFxJcBnBQU0n1Q1M6zXsdSfjttewt3Ub9WfYl8JlQ9ho8yCCy4ArqY+ZXoABmJoDgdUEVxEc3HRSDcE3KewPvAO4pem86XUL8A4K+3c/Q+sn/sEEVwGrqY+RNpIB2DSvB75KsIZgYfNphTUUDqS+nP1u83mb7rvAoRQOpLCm+bRgIcEa6hmer28+bwIZgM2zD3ARwbcJFg1hJ+MbKbyF+vK27U62U3MDsIDCWyjc2HRS3WF3EcG3gYuoj4E2kQGYHnOBlcB3CA4fwk7GayksBObDEPayf27XAvMpLKQ0/o6FusPu4cB3qPf13KbzkjAA02sO8EXgewRHEcxsOq2wjsIiYB71ja9hXNgR3ax5FBZRWNd42kyCo4DvUe/bOU3nJWMA2phNvYrt+wSLh7CT8e0UjqC+HF4NTb7M8g/dbe9D4QgKtzeY8ZS6w+5i4PvU+3J203lJGYC2XgOcAdxCsIRg66bTCndSOBp4HXAJ07OT8ZPdbb2OwtEU7pyG23xuwdYES6ifJJxBvQ/ViAEYjldSv2p6HcEygllNpxXupnAs9QspvgI8vgm38nj3/+5B4VgKd0/nEp8hmEWwjLrD7qnU+0yNGYDh2gn4DHUn4+MJtm06rfAzCkuB3anfULMx59xv6P7s7hSWUvhZyyUSbEtwPHWH3c9Q7yMNiQHoxw7Ap4EfE5xEsF3TaYV7KSyn/h79BZ59J+P13X+bTWE5hXubrinYjuAk4MfU+2KHpvP0rDLuCDSKHqZ+eeQFFP6v+bS69dUp1H3voP6Nf+aQtkr7M+DDwFJoHL7R1/uOQAZgtDzKUxuY/m/zadH9rVt4cAiz/pynNtp01+Sq9wC0/ZxaU/Vi6qaUHyRYBZxH4f5m04bzxN8JWA4cC43f/NSU+R7AaJoF3TviwanEGL4jHryS6D75qD+LT/4RZABG29bQfSYenE6MwWfiwWsITqd+jr8EGp/7oM1iAMbDlsD7qWcWriDYte8FPUOwK8EK6pl77yfxRpvjxACMl5nA0dRrDc4lRuC8+GAOwbnUc/WPxveVxooBGE9bAEcANxGsJHq4Mi6YS7ASuKlbS/oddseRARhvM4BFwA0EFxJDuDY+2IfgQuoeAIvwGBprPniToQBvA9YQXNKdWzG9gnkElwBrulnusDsBDMDkeStwNcFqgvmbfWvBfILV1JO13rrZt6eRYgAm10HA5QRXEJuwQ26wgOAK4PLutjSBDMDkeyNwKcHVBIe84J8ODiG4mvoNym9svTj1ywDkMQ9YRXA9wduftoFp3Wjz7QTXA6u6P6sE/Mw2n72pG338F8Hnu393IvWryZSMAchrT+plwErMXwGkxAyAlJgBkBIzAFJiBkBKzABIiRkAKTEDICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMQMgJSYAZASMwBSYgZASswASIkZACkxAyAlZgCkxAyAlJgBkBIzAFJiBkBKzABIiRkAKTEDICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMQMgJSYAZASMwBSYgZASswASIkZACmxmX0vAHgAWNX3IqQePND3AkoQfa9BUk/8FUBKzABIiRkAKTEDICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMQMgJSYAZASMwBSYgZASswASIkZACkxAyAlZgCkxAyAlJgBkBIzAFJiBkBKzABIiRkAKTEDICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMQMgJSYAZASMwBSYgZASswASIkZACkxAyAlZgCkxAyAlJgBkBIzAFJiBkBKzABIiRkAKTEDICVmAKTEDICUmAGQEjMAUmIGQErMAEiJGQApMQMgJWYApMQMgJSYAZASMwBSYgZASswASIkZACkxAyAlZgCkxAyAlJgBkBIzAFJiBkBKzABIif0/LLGX8b5Q3EwAAAAASUVORK5CYII="/>
</defs>
</svg>

+ 9
- 0
public/images/maps.svg
文件差異過大導致無法顯示
查看文件


+ 9
- 0
public/images/pin.svg
文件差異過大導致無法顯示
查看文件


二進制
public/images/product-card-image.jpg 查看文件


+ 0
- 0
public/images/profile.svg 查看文件


部分文件因文件數量過多而無法顯示

Loading…
取消
儲存