| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; | |||||
| 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; |
| 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; |
| 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; | |||||
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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> | |||||
| ); | |||||
| } |
| 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; |
| 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> | |||||
| ); | |||||
| }; |
| 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>, | |||||
| }, | |||||
| ]; |
| import { Box } from '@mui/system'; | |||||
| const PageWrapper = ({ children }) => { | |||||
| return <Box sx={{ py: 10, height: '100%', width: '100%' }}>{children}</Box>; | |||||
| }; | |||||
| export default PageWrapper; |
| 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; |
| 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; |
| const { CircularProgress, Box } = require('@mui/material'); | |||||
| const LoadingSpinner = () => { | |||||
| return ( | |||||
| <Box display="flex" justifyContent="center" sx={{ mt: 5 }}> | |||||
| <CircularProgress /> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| export default LoadingSpinner; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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'; |
| 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; |
| 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, | |||||
| } | |||||
| ); | |||||
| }; |
| 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, | |||||
| } | |||||
| ); | |||||
| }; |
| "lint": "next lint" | "lint": "next lint" | ||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "@emotion/react": "^11.10.4", | |||||
| "@emotion/styled": "^11.10.4", | |||||
| "@mui/codemod": "^5.10.8", | |||||
| "@mui/icons-material": "^5.10.6", | |||||
| "@mui/material": "^5.10.8", | |||||
| "@stripe/stripe-js": "^1.39.0", | |||||
| "@tanstack/react-query": "^4.10.3", | |||||
| "@types/mongodb": "^4.0.7", | "@types/mongodb": "^4.0.7", | ||||
| "formik": "^2.2.9", | |||||
| "next": "12.3.1", | "next": "12.3.1", | ||||
| "next-auth": "^4.13.0", | |||||
| "next-i18next": "^11.3.0", | |||||
| "nookies": "^2.5.2", | |||||
| "react": "18.2.0", | "react": "18.2.0", | ||||
| "react-dom": "18.2.0" | |||||
| "react-dom": "18.2.0", | |||||
| "react-i18next": "^11.18.6", | |||||
| "yup": "^0.32.11" | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@tanstack/react-query-devtools": "^4.11.0", | |||||
| "@types/node": "18.8.3", | "@types/node": "18.8.3", | ||||
| "@types/react": "18.0.21", | "@types/react": "18.0.21", | ||||
| "@types/react-dom": "18.0.6", | "@types/react-dom": "18.0.6", |
| 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 '../styles/globals.css' | ||||
| import type { AppProps } from 'next/app' | 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); |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| import type { NextPage } from 'next' | import type { NextPage } from 'next' | ||||
| import { useSession } from 'next-auth/react'; | |||||
| import Head from 'next/head' | 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 | export default Home |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| <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> |
| <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> |
| <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> |
| <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> |
| <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> |
| <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> |
| <?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> |
| <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> |