Ver código fonte

feat: front typescript

pull/1/head
Selena Simic 3 anos atrás
pai
commit
dc86142acd
100 arquivos alterados com 4927 adições e 66 exclusões
  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. 15
    1
      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. BIN
      public/images/Hero-Image.png
  78. 9
    0
      public/images/Instagram.svg
  79. BIN
      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. BIN
      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. BIN
      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. BIN
      public/images/product-card-image.jpg
  100. 0
    0
      public/images/profile.svg

+ 51
- 0
components/buttons/load-more/LoadMore.tsx Ver arquivo

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ 15
- 1
package.json Ver arquivo

@@ -9,12 +9,26 @@
"lint": "next lint"
},
"dependencies": {
"@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",
"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-dom": "18.2.0",
"react-i18next": "^11.18.6",
"yup": "^0.32.11"
},
"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 Ver arquivo

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

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

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

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

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

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

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

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

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

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

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

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

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

@@ -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
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


BIN
public/images/Hero-Image.png Ver arquivo


+ 9
- 0
public/images/Instagram.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


BIN
public/images/Item 2.png Ver arquivo


+ 4
- 0
public/images/Play.svg Ver arquivo

@@ -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
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 9
- 0
public/images/Twitter.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 3
- 0
public/images/arrow.svg Ver arquivo

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

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

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

BIN
public/images/coffee-bag 1.png Ver arquivo


+ 9
- 0
public/images/coffee-beans-icon.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 9
- 0
public/images/coffee-beans.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 9
- 0
public/images/coffee-machine.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 9
- 0
public/images/coffee-mug.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 9
- 0
public/images/factory.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


BIN
public/images/image-one.jpg Ver arquivo


+ 3
- 0
public/images/line.svg Ver arquivo

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

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

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

@@ -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
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


+ 9
- 0
public/images/pin.svg
Diferenças do arquivo suprimidas por serem muito extensas
Ver arquivo


BIN
public/images/product-card-image.jpg Ver arquivo


+ 0
- 0
public/images/profile.svg Ver arquivo


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff

Carregando…
Cancelar
Salvar