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