Преглед на файлове

feat: front typescript

pull/1/head
Selena Simic преди 3 години
родител
ревизия
dc86142acd
променени са 100 файла, в които са добавени 4927 реда и са изтрити 66 реда
  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. Двоични данни
      public/images/Hero-Image.png
  78. 9
    0
      public/images/Instagram.svg
  79. Двоични данни
      public/images/Item 2.png
  80. 4
    0
      public/images/Play.svg
  81. 25
    0
      public/images/Stars.svg
  82. 9
    0
      public/images/Twitter.svg
  83. 3
    0
      public/images/arrow.svg
  84. 3
    0
      public/images/cart.svg
  85. 5
    0
      public/images/clock.svg
  86. Двоични данни
      public/images/coffee-bag 1.png
  87. 9
    0
      public/images/coffee-beans-icon.svg
  88. 9
    0
      public/images/coffee-beans.svg
  89. 9
    0
      public/images/coffee-machine.svg
  90. 9
    0
      public/images/coffee-mug.svg
  91. 9
    0
      public/images/factory.svg
  92. Двоични данни
      public/images/image-one.jpg
  93. 3
    0
      public/images/line.svg
  94. 3
    0
      public/images/lock.svg
  95. 57
    0
      public/images/logout.svg
  96. 9
    0
      public/images/mail.svg
  97. 9
    0
      public/images/maps.svg
  98. 9
    0
      public/images/pin.svg
  99. Двоични данни
      public/images/product-card-image.jpg
  100. 0
    0
      public/images/profile.svg

+ 51
- 0
components/buttons/load-more/LoadMore.tsx Целия файл

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

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

export default LoadMore;

+ 24
- 0
components/cards/card-container/CardContainer.tsx Целия файл

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

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

export default CardContainer;

+ 176
- 0
components/cards/cart-card/CartCard.tsx Целия файл

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

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

export default CartCard;

+ 69
- 0
components/cards/data-card/DataCard.tsx Целия файл

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

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

export default DataCard;

+ 44
- 0
components/cards/order-card/OrderCard.tsx Целия файл

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

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


export default OrderCard;

+ 73
- 0
components/cards/order-summary-card/OrderSummaryCard.tsx Целия файл

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

const OrderSummaryCard = ({ data }) => {
const { t } = useTranslation('cart');
const router = useRouter();
return (
<Card sx={{ p: 3, width: '100%', mb: 2, backgroundColor: '#f1f1f1' }}>
<Typography
sx={{
fontSize: 26,
color: 'primary.main',
textAlign: 'center',
width: '100%',
}}
>
{t('cart:orderTitle')}
</Typography>
<Typography sx={{ mt: 4 }}>
{t('cart:itemsTotal')}
{data.totalPrice.toFixed(2)}
</Typography>
<Typography sx={{ mt: 1.5 }}>{t('cart:shipping')}</Typography>
<Typography sx={{ mt: 1.5, mb: 1.5 }}>
{t('cart:total')}
{data.totalPrice.toFixed(2)}
</Typography>
<Divider />
<Box sx={{ textAlign: 'center', mt: 4, width: '100%' }}>
<Button
disableRipple
sx={{
'&.Mui-disabled': {
backgroundColor: '#0066ff',
color: '#fff',
opacity: '0.6',
},
'&:hover': {
backgroundColor: '#0066ff',
color: 'white',
boxShadow: 'none',
},
backgroundColor: '#0066ff',
color: 'white',
textTransform: 'none',
px: 2,
}}
startIcon={
<Image src="/images/lock.svg" alt="lock" width={18} height={18} />
}
disabled={data.totalQuantity > 0 ? false : true}
onClick={() => {
router.push('/checkout');
setCookie(null, 'checkout-session', 'active', {
maxAge: 3600,
expires: new Date(Date.now() + 3600),
path: '/',
});
}}
>
{t('cart:proceed')}
</Button>
</Box>
<Typography sx={{ mt: 3, fontSize: 13 }}>{t('cart:infoMsg')}</Typography>
</Card>
);
};

export default OrderSummaryCard;

+ 74
- 0
components/cart-content/CartContent.tsx Целия файл

@@ -0,0 +1,74 @@
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import { destroyCookie } from 'nookies';
import { useEffect, useState } from 'react';
import { useStore, useStoreUpdate } from '../../store/cart-context';
import CartCard from '../cards/cart-card/CartCard';
import OrderSummaryCard from '../cards/order-summary-card/OrderSummaryCard';
import EmptyCart from '../empty-cart/EmptyCart';
import ContentContainer from '../layout/content-wrapper/ContentContainer';
import PageWrapper from '../layout/page-wrapper/PageWrapper';
import StepTitle from '../layout/steps-title/StepTitle';

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

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

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

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

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

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

export default CartContent;

+ 76
- 0
components/checkout-content/CheckoutContent.tsx Целия файл

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

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

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

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

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

const submitHandler = (formValues) => {
addCheckoutValue(
cartData,
{ ...formValues, email: session.user.email },
session.user._id
);
setCookie(null, 'shipping-session', 'active', {
maxAge: 3600,
expires: new Date(Date.now() + 3600),
path: '/',
});
router.push('/shipping');
};

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

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

export default CheckoutContent;

+ 120
- 0
components/company-info/CompanyInfo.tsx Целия файл

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

export default CompanyInfo;

+ 22
- 0
components/empty-cart/EmptyCart.tsx Целия файл

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

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

export default EmptyCart;

+ 35
- 0
components/features/FeatureItem.tsx Целия файл

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

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

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

export default FeatureItem;

+ 89
- 0
components/features/Features.tsx Целия файл

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

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

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

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

export default Features;

+ 23
- 0
components/features/items.ts Целия файл

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

+ 29
- 0
components/filter-sort/FilterSort.tsx Целия файл

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

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

export default FilterSort;

+ 75
- 0
components/forms/contact/ContactForm.tsx Целия файл

@@ -0,0 +1,75 @@
import { Box, Button, Paper, TextField } from '@mui/material';
import { useFormik } from 'formik';
import React, { useState } from 'react';
import { contactSchema } from '../../../schemas/contactSchema';
import { useCheckoutData } from '../../../store/checkout-context';
import ErrorMessageComponent from '../../mui/ErrorMessageComponent';

const ContactForm = ({ submitHandler }) => {
const [error] = useState({ hasError: false, errorMessage: '' });
const { checkoutStorage } = useCheckoutData();

const handleSubmit = async (values) => {
submitHandler(values.email);
};

const formik = useFormik({
initialValues: {
email: checkoutStorage ? checkoutStorage.userInfo.email : '',
},
validationSchema: contactSchema,
onSubmit: handleSubmit,
validateOnBlur: true,
enableReinitialize: true,
});

return (
<Paper
sx={{ p: 3, width: '90%', ml: 12, mt: 2, backgroundColor: '#f2f2f2' }}
elevation={3}
>
<Box
sx={{
width: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
{error.hasError && <ErrorMessageComponent error={error.errorMessage} />}
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="email"
label="Email"
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
fullWidth
/>
<Button
type="submit"
variant="contained"
sx={{
mt: 3,
mb: 2,
backgroundColor: '#CBA213',
height: 50,
width: 150,
textTransform: 'none',
color: 'white',
}}
>
Submit Details
</Button>
</Box>
</Box>
</Paper>
);
};

export default ContactForm;

+ 135
- 0
components/forms/contact/ContactPageForm.tsx Целия файл

@@ -0,0 +1,135 @@
import {
Box,
Button,
Container,
Grid,
TextField,
Typography,
} from '@mui/material';
import { useFormik } from 'formik';
import { useTranslation } from 'next-i18next';
import Link from 'next/link';
import React, { useState } from 'react';
import { BASE_PAGE } from '../../../constants/pages';
import { postQuestion } from '../../../requests/question/postQuestionRequest';
import { contactPageSchema } from '../../../schemas/contactSchema';
import Notification from '../../notification/Notification';
const ContactPageForm = () => {
const { t } = useTranslation('contact');
const [open, setOpen] = useState(false);
const handleSubmit = async (values) => {
try {
postQuestion(values);
setOpen(true);
} catch (error) {
console.log(error);
}
};
const handleCloseNotification = () => {
setOpen(false);
};
const formik = useFormik({
initialValues: {
firstName: '',
lastName: '',
email: '',
message: '',
},
validationSchema: contactPageSchema,
onSubmit: handleSubmit,
validateOnBlur: true,
enableReinitialize: true,
});
return (
<Container component="main" maxWidth="md" sx={{ mb: '60px' }}>
<Notification
open={open}
notification={t('contact:notification')}
handleCloseNotification={handleCloseNotification}
/>
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography fontSize={48}>{t('contact:title')}</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="firstName"
label={t('contact:firstName')}
margin="normal"
value={formik.values.firstName}
onChange={formik.handleChange}
error={formik.touched.firstName && Boolean(formik.errors.firstName)}
helperText={formik.touched.firstName && formik.errors.firstName}
autoFocus
fullWidth
/>
<TextField
name="lastName"
label={t('contact:lastName')}
margin="normal"
value={formik.values.lastName}
onChange={formik.handleChange}
error={formik.touched.lastName && Boolean(formik.errors.lastName)}
helperText={formik.touched.lastName && formik.errors.lastName}
autoFocus
fullWidth
/>
<TextField
name="email"
label={t('contact:email')}
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
autoFocus
fullWidth
/>
<TextField
name="message"
label={t('contact:message')}
multiline
margin="normal"
value={formik.values.message}
onChange={formik.handleChange}
error={formik.touched.message && Boolean(formik.errors.message)}
helperText={formik.touched.message && formik.errors.message}
rows={4}
autoFocus
fullWidth
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('contact:sendBtn')}
</Button>
<Grid container justifyContent="center">
<Link href={BASE_PAGE}>
<Typography>{t('contact:back')}</Typography>
</Link>
</Grid>
</Box>
</Box>
</Container>
);
};
export default ContactPageForm;

+ 83
- 0
components/forms/forgot-password/ForgotPasswordForm.tsx Целия файл

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

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

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

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

return (
<Container component="main" maxWidth="md">
<Box
sx={{
marginTop: 32,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Typography component="h1" variant="h5">
{t('forgotPass:Title')}
</Typography>
<Box
component="form"
onSubmit={formik.handleSubmit}
sx={{ position: 'relative', mt: 1, p: 1 }}
>
<TextField
name="email"
label={t('forms:Email')}
margin="normal"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
autoFocus
fullWidth
/>
<Button
type="submit"
variant="contained"
sx={{ mt: 3, mb: 2 }}
fullWidth
>
{t('forgotPass:SendBtn')}
</Button>
<Grid container justifyContent="center">
<Link href={LOGIN_PAGE}>
<Typography sx={{ cursor: 'pointer' }}>
{t('common:Back')}
</Typography>
</Link>
</Grid>
</Box>
</Box>
</Container>
);
};

export default ForgotPasswordForm;

+ 155
- 0
components/forms/login/LoginForm.tsx Целия файл

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

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

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

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

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

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

export default LoginForm;

+ 263
- 0
components/forms/register/RegisterForm.tsx Целия файл

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

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

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

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

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

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

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

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

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

export default RegisterForm;

+ 164
- 0
components/forms/shipping-details/ShippingDetailsForm.tsx Целия файл

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

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

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

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

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

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

export default ShippingDetailsForm;

+ 11
- 0
components/grid-item/GridItem.tsx Целия файл

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

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

export default GridItem;

+ 142
- 0
components/hero/Hero.tsx Целия файл

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

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

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

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

export default Hero;

+ 23
- 0
components/layout/base-layout/Layout.tsx Целия файл

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

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

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

export default Layout;

+ 18
- 0
components/layout/content-wrapper/ContentContainer.tsx Целия файл

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

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

export default ContentContainer;

+ 148
- 0
components/layout/footer/Footer.tsx Целия файл

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

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

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

export default Footer;

+ 127
- 0
components/layout/navbar/DesktopNav.tsx Целия файл

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

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

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

export default DesktopNav;

+ 105
- 0
components/layout/navbar/MainNav.tsx Целия файл

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

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

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

const router = useRouter();

const { data: session } = useSession();

const { totalQuantity } = useStore();

const { clearUser } = useUserUpdate();

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

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

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

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

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

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

+ 135
- 0
components/layout/navbar/MobileNav.tsx Целия файл

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

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

}

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

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

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

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

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

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

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

export default MobileNav;

+ 87
- 0
components/layout/navbar/NavItem.tsx Целия файл

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

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

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

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

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

+ 29
- 0
components/layout/navbar/navItems.tsx Целия файл

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

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

+ 7
- 0
components/layout/page-wrapper/PageWrapper.tsx Целия файл

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

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

export default PageWrapper;

+ 55
- 0
components/layout/steps-title/StepTitle.tsx Целия файл

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

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

export default StepTitle;

+ 35
- 0
components/loader/Loader.tsx Целия файл

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

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

export default Loader;

+ 11
- 0
components/loader/basic-spinner/LoadSpinner.tsx Целия файл

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

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

export default LoadingSpinner;

+ 56
- 0
components/loader/route-loader/CircularIndeterminate.tsx Целия файл

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

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

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

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

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

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

export default CircularIndeterminate;

+ 15
- 0
components/mui/ErrorMessageComponent.tsx Целия файл

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

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

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

export default ErrorMessageComponent;

+ 22
- 0
components/notification/Notification.tsx Целия файл

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

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

export default Notification;

+ 12
- 0
components/page-description/PageDescription.tsx Целия файл

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

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

export default PageDescription;

+ 105
- 0
components/product-card/ProductCard.tsx Целия файл

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

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

export default ProductCard;

+ 29
- 0
components/product-type/ProductType.tsx Целия файл

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

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

export default ProductType;

+ 60
- 0
components/products-content/ProductsContent.tsx Целия файл

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

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

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

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

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

export default ProductsContent;

+ 43
- 0
components/products-grid/ProductsGrid.tsx Целия файл

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

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

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

export default ProductsGrid;

+ 43
- 0
components/products-hero/ProductsHero.tsx Целия файл

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

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

export default ProductsHero;

+ 78
- 0
components/products/featured-product/FeaturedProduct.tsx Целия файл

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

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

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

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

export default FeaturedProduct;

+ 19
- 0
components/products/featured-product/ProductImage.tsx Целия файл

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

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

export default ProductImage;

+ 154
- 0
components/products/featured-product/ProductInfo.tsx Целия файл

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

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

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

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

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


export default ProductInfo;

+ 27
- 0
components/products/featured-products-list/FeaturedPorductsList.tsx Целия файл

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

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

export default FeaturedProductsList;

+ 88
- 0
components/profile-content/ProfileContent.tsx Целия файл

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

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

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

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

const mapOrdersToDom = () =>
orders.slice(-4).map((order, i) => (
<OrderCard
key={i}
data={{
date: order.time.split('T')[0],
name: order.shippingAddress.fullName,
totalPrice: order.totalPrice,
}}
></OrderCard>
));

return (
<PageWrapper>
<StepTitle title={t('profile:title')} />
<Notification
open={open}
handleCloseNotification={handleCloseNotification}
notification={t('profile:notification')}
/>

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

export default ProfileContent;

+ 195
- 0
components/review-content/ReviewContent.tsx Целия файл

@@ -0,0 +1,195 @@
import { Button, Typography } from '@mui/material';
import { Box } from '@mui/system';
import { useTranslation } from 'next-i18next';
import { useRouter } from 'next/router';
import { destroyCookie } from 'nookies';

import { useEffect, useState } from 'react';
import { postOrder } from '../../requests/products/postOrderRequest';
import { useStoreUpdate } from '../../store/cart-context';
import {
useCheckoutData,
useCheckoutDataUpdate,
} from '../../store/checkout-context';
import PageWrapper from '../layout/page-wrapper/PageWrapper';
import StepTitle from '../layout/steps-title/StepTitle';

let initialRender = true;

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

const router = useRouter();

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

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

export default ReviewContent;

+ 128
- 0
components/shipping-content/ShippingContent.tsx Целия файл

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

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

const router = useRouter();

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

const handleOpen = (type) => setOpen({ isOpen: true, type });
const handleClose = () => setOpen({ isOpen: false, type: '' });

const handleChangeShipping = (values) => {
changeShippingData(values);
handleClose();
};

const handleChangeContact = (values) => {
changeContact(values);
handleClose();
};

const handleStripePayment = () => {
stripe({
lineItems: checkoutData.products.map((el) => ({
price: el.product.stripeID,
quantity: el.quantity,
})),
userInfo: checkoutData.userInfo,
});
setCookie(null, 'review-session', 'active', {
maxAge: 3600,
expires: new Date(Date.now() + 3600),
path: '/',
});
};

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

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

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

export default ShippingContent;

+ 50
- 0
components/shipping-content/shipping-btnGroup/ButtonGroup.tsx Целия файл

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

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

export default ButtonGroup;

+ 84
- 0
components/shipping-content/shipping-data/ShippingData.tsx Целия файл

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

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

return (
<>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
backgroundColor: '#f2f2f2',
alignItems: 'center',
mb: 2,
borderRadius: 2,
p: 1,
}}
>
<Typography sx={{ fontSize: 18, fontWeight: 600 }}>
{t('shipping:contact')}
</Typography>
<Typography>{email}</Typography>
<Button
sx={{
height: 35,
minWidth: { md: 125, xs: 90 },
fontSize: 15,
textTransform: 'none',
backgroundColor: '#CBA213',
color: 'white',
}}
onClick={() => {
handleOpen('Contact');
}}
>
{t('shipping:changeBtn')}
</Button>
</Box>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
backgroundColor: '#f2f2f2',
alignItems: 'center',
mb: 2,
borderRadius: 2,
p: 1,
}}
>
<Typography
sx={{
fontSize: { md: 18, xs: 16 },
fontWeight: 600,
mr: { xs: 1, sm: 0 },
}}
>
{t('shipping:shipping')}
</Typography>
<Typography>
{address} | {city} | {postcode}
</Typography>
<Button
sx={{
height: 35,
minWidth: { md: 125, xs: 90 },
fontSize: 15,
textTransform: 'none',
backgroundColor: '#CBA213',
color: 'white',
}}
onClick={() => {
handleOpen('Shipping');
}}
>
{t('shipping:changeBtn')}
</Button>
</Box>
</>
);
};

export default ShippingData;

+ 39
- 0
components/shipping-content/shipping-modal/ShippingModal.tsx Целия файл

@@ -0,0 +1,39 @@
import { Modal } from '@mui/material';
import { Box } from '@mui/system';
import ContactForm from '../../forms/contact/ContactForm';
import ShippingDetailsForm from '../../forms/shipping-details/ShippingDetailsForm';

const ShippingModal = ({
open,
handleClose,
handleChangeShipping,
handleChangeContact,
}) => {
return (
<Modal
open={open.isOpen}
onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<Box
sx={{
width: { xs: '90%', md: '50%' },
top: '50%',
left: '50%',
position: 'absolute',
transform: 'translate(-50%, -50%)',
}}
>
{open.type === 'Shipping' && (
<ShippingDetailsForm submitHandler={handleChangeShipping} />
)}
{open.type === 'Contact' && (
<ContactForm submitHandler={handleChangeContact} />
)}
</Box>
</Modal>
);
};

export default ShippingModal;

+ 34
- 0
components/sort/Sort.tsx Целия файл

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

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

export default Sort;

+ 91
- 0
components/tab-content/TabContent.tsx Целия файл

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

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

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

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

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

export default TabContent;

+ 27
- 0
components/tab-panel/TabPanel.tsx Целия файл

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

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

export default TabPanel;

+ 12
- 0
constants/pages.ts Целия файл

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

+ 24
- 0
hooks/useCalculateTotal.ts Целия файл

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

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

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

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

return {
total,
};
};

export default useCalculateTotal;

+ 14
- 0
hooks/useFetchProductData.ts Целия файл

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

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

+ 24
- 0
hooks/useInfiniteQuery.ts Целия файл

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

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

+ 15
- 1
package.json Целия файл

@@ -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 Целия файл

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

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

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

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

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

+ 17
- 0
pages/_document.tsx Целия файл

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

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

export default MyDocument;

+ 35
- 0
pages/auth/forgot-password/index.tsx Целия файл

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

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

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

return <ForgotPasswordForm />;
};

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

export default ForgotPasswordPage;

+ 31
- 0
pages/auth/index.tsx Целия файл

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

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

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

return <LoginForm />;
};

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

export default AuthPage;

+ 31
- 0
pages/auth/register/index.tsx Целия файл

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

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

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

return <RegisterForm />;
};

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

export default RegisterPage;

+ 17
- 0
pages/cart/index.tsx Целия файл

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

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

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

export default CartPage;

+ 32
- 0
pages/checkout/index.tsx Целия файл

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

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

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

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

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

export default CheckoutPage;

+ 18
- 0
pages/contact/index.tsx Целия файл

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

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

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

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

export default Contact;

+ 57
- 62
pages/index.tsx Целия файл

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

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

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

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

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

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

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

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

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

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

export default Home

+ 114
- 0
pages/products/[customId].tsx Целия файл

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

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

const router = useRouter();

const { customId } = router.query;

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

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

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

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

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

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

const queryClient = new QueryClient();

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

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

export default SingleProduct;

+ 17
- 0
pages/products/index.tsx Целия файл

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

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

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

export default Products;

+ 37
- 0
pages/profile/index.tsx Целия файл

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

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

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

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

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

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

export default ProfilePage;

+ 29
- 0
pages/review/index.tsx Целия файл

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

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

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

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

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

export default ReviewPage;

+ 31
- 0
pages/shipping/index.tsx Целия файл

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

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

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

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

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

+ 9
- 0
public/images/Facebook.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


Двоични данни
public/images/Hero-Image.png Целия файл


+ 9
- 0
public/images/Instagram.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


Двоични данни
public/images/Item 2.png Целия файл


+ 4
- 0
public/images/Play.svg Целия файл

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

+ 25
- 0
public/images/Stars.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 9
- 0
public/images/Twitter.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 3
- 0
public/images/arrow.svg Целия файл

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

+ 3
- 0
public/images/cart.svg Целия файл

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

+ 5
- 0
public/images/clock.svg Целия файл

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

Двоични данни
public/images/coffee-bag 1.png Целия файл


+ 9
- 0
public/images/coffee-beans-icon.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 9
- 0
public/images/coffee-beans.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 9
- 0
public/images/coffee-machine.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 9
- 0
public/images/coffee-mug.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 9
- 0
public/images/factory.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


Двоични данни
public/images/image-one.jpg Целия файл


+ 3
- 0
public/images/line.svg Целия файл

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

+ 3
- 0
public/images/lock.svg Целия файл

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

+ 57
- 0
public/images/logout.svg Целия файл

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 384.971 384.971" style="enable-background:new 0 0 384.971 384.971;" xml:space="preserve">
<g>
<g id="Sign_Out">
<path fill='#664C47' d="M180.455,360.91H24.061V24.061h156.394c6.641,0,12.03-5.39,12.03-12.03s-5.39-12.03-12.03-12.03H12.03
C5.39,0.001,0,5.39,0,12.031V372.94c0,6.641,5.39,12.03,12.03,12.03h168.424c6.641,0,12.03-5.39,12.03-12.03
C192.485,366.299,187.095,360.91,180.455,360.91z"/>
<path fill='#664C47' d="M381.481,184.088l-83.009-84.2c-4.704-4.752-12.319-4.74-17.011,0c-4.704,4.74-4.704,12.439,0,17.179l62.558,63.46H96.279
c-6.641,0-12.03,5.438-12.03,12.151c0,6.713,5.39,12.151,12.03,12.151h247.74l-62.558,63.46c-4.704,4.752-4.704,12.439,0,17.179
c4.704,4.752,12.319,4.752,17.011,0l82.997-84.2C386.113,196.588,386.161,188.756,381.481,184.088z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

+ 9
- 0
public/images/mail.svg Целия файл

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

+ 9
- 0
public/images/maps.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 9
- 0
public/images/pin.svg
Файловите разлики са ограничени, защото са твърде много
Целия файл


Двоични данни
public/images/product-card-image.jpg Целия файл


+ 0
- 0
public/images/profile.svg Целия файл


Някои файлове не бяха показани, защото твърде много файлове са промени

Loading…
Отказ
Запис