Procházet zdrojové kódy

feat: added product and single product pages

single-product
Lazar Kostic před 3 roky
rodič
revize
30c5dd1ab5

+ 3
- 0
.vscode/settings.json Zobrazit soubor

@@ -4,5 +4,8 @@
"editor.codeActionsOnSave": {
"source.fixAll": true,
"source.organizeImports": true
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}

+ 29
- 0
components/filter-sort/FilterSort.jsx Zobrazit soubor

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

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

export default FilterSort;

+ 35
- 0
components/loader/Loader.js Zobrazit soubor

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

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

export default Loader;

+ 30
- 13
components/product-card/ProductCard.jsx Zobrazit soubor

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

const ProductCard = () => {
const ProductCard = ({ product }) => {
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: '590px',
height: '100%',
border: 'none',
mb: '75px',
backgroundColor: '#F5ECD4',
}}
>
<Box width="100%">
<Image
src="/images/product-card-image.jpg"
alt="product image"
width={373.33}
height={249}
/>
<NextLink
style={{ cursor: 'pointer' }}
href={`/products/${product.customID}`}
passHref
>
<Image
src="/images/product-card-image.jpg"
alt="product image"
width={630}
height={390}
/>
</NextLink>
</Box>
<Box
width="100%"
@@ -29,17 +45,18 @@ const ProductCard = () => {
}}
>
<Typography fontSize="24px" align="center" pt={1} pb={3}>
MINIMALIST PRINTED MUG
{product.name}
</Typography>
<Typography align="center" fontSize="18px" m={2}>
Our simple and sturdy mugs are made to last. With a minimalist desings
you will soon be enjoying your next brew.
{product.description}
</Typography>
<Typography fontSize="24px" align="center" pt={4}>
$20
${product.price}
</Typography>
<Box textAlign="center" mt={1}>
<Button
disabled={inCart}
onClick={() => addProductToCart(1)}
sx={{
backgroundColor: '#CBA213',
height: 50,
@@ -47,7 +64,7 @@ const ProductCard = () => {
color: 'white',
}}
>
Add to cart
{inCart ? 'In Cart' : 'Add to cart'}
</Button>
</Box>
</Box>

+ 3
- 2
components/product-type/ProductType.jsx Zobrazit soubor

@@ -12,8 +12,9 @@ const ProductType = ({ productType, handleProductTypeChange }) => {
value={productType}
onChange={handleProductTypeChange}
>
<MenuItem value="asc">Name - A-Z</MenuItem>
<MenuItem value="desc">Name - Z-A</MenuItem>
<MenuItem value="All">All</MenuItem>
<MenuItem value="Coffee">Coffee</MenuItem>
<MenuItem value="Mug">Mug</MenuItem>
</Select>
</FormControl>
</>

+ 119
- 0
components/products-grid/ProductsGrid.jsx Zobrazit soubor

@@ -0,0 +1,119 @@
import { Button, Container, Grid } from '@mui/material';
import { Box } from '@mui/system';
import Image from 'next/image';
import { useMemo, useState } from 'react';
import { useFetchProductsByCategory } from '../../hooks/useFetchProductData';
import { compare } from '../../utils/helpers/sortHelpers';
import ProductCard from '../product-card/ProductCard';

const ProductsGrid = ({
allProducts,
hasNextPage,
productType,
fetchNextPage,
sort,
}) => {
const productsPerPage = 9;
const [next, setNext] = useState(productsPerPage);

const { data: filteredData } = useFetchProductsByCategory(productType);

const allItems = useMemo(
() => allProducts?.pages?.flatMap((page) => page.product),
[allProducts]
);

const dataToDisplay =
productType === 'All' || productType === ''
? allItems.sort(compare('name', sort)).map((item) => (
<Grid key={item._id} item md={4} sm={6} xs={12} sx={{ mb: '100px' }}>
<ProductCard product={item} />
</Grid>
))
: filteredData?.productsByCategory
.slice(0, next)
.sort(compare('name', sort))
.map((item) => (
<Grid
key={item._id}
item
md={4}
sm={6}
xs={12}
sx={{ mb: '100px' }}
>
<ProductCard product={item} />
</Grid>
));

const handleMoreProducts = () => {
setNext(next + productsPerPage);
};

return (
<Container
sx={{
mt: 10,
}}
>
<Grid container spacing={2}>
{dataToDisplay}
</Grid>
<Box textAlign="center" mt={-5} mb={5}>
{hasNextPage && (productType === 'All' || productType === '') && (
<Button
onClick={fetchNextPage}
startIcon={
<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', // theme.palette.primary.main
color: 'white',
},
}}
>
Load More
</Button>
)}

{filteredData && next < filteredData.productsByCategory.length && (
<Button
onClick={handleMoreProducts}
startIcon={
<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', // theme.palette.primary.main
color: 'white',
},
}}
>
Load More
</Button>
)}
</Box>
</Container>
);
};

export default ProductsGrid;

+ 11
- 75
components/products-hero/ProductsHero.jsx Zobrazit soubor

@@ -1,106 +1,42 @@
import { Button, Container, Grid, Typography } from '@mui/material';
import { Container, Typography } from '@mui/material';
import { Box } from '@mui/system';
import Image from 'next/image';
import ProductCard from '../product-card/ProductCard';
import ProductType from '../product-type/ProductType';
import Sort from '../sort/sort';

const ProductsHero = () => {
return (
<Box
sx={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<Container
maxWidth="lg"
sx={{
width: '1273px',
height: '350px',
mt: 25,
mb: 10,
}}
>
<Typography
fontFamily={'body1.fontFamily'}
height="120px"
fontSize="64px"
align="center"
color="primary.main"
mb={3}
sx={{
fontSize: { md: '64px', sm: '46px', xs: '32px' },
}}
>
Welcome to our Store!
</Typography>
<Typography fontSize="24px" align="center">
<Typography
sx={{ fontSize: { xs: '16px', sm: '18px', md: '24px' } }}
align="center"
>
Our focus is to bring you the very best in the world of coffee.
Everything from fresh coffee beans, the best coffee powders and
capsules as well as other miscellaneous items such as cups and mugs.
Take a look to see if anything takes your fancy.
</Typography>
</Container>
<Box textAlign="center" width="100%">
<Sort />
<ProductType />
</Box>
<Container
sx={{
mt: 10,
}}
>
<Grid container spacing={2}>
<Grid item md={4} xs={12} sx={{ height: '500px' }}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
<Grid item md={4} xs={12}>
<ProductCard />
</Grid>
</Grid>
<Box textAlign="center" mt={-3} mb={5}>
<Button
startIcon={
<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', // theme.palette.primary.main
color: 'white',
},
}}
>
Load More
</Button>
</Box>
</Container>
</Box>
);
};

+ 1
- 1
components/products/featured-product/ProductInfo.jsx Zobrazit soubor

@@ -120,6 +120,6 @@ ProductInfo.propTypes = {
bColor: PropType.string,
side: PropType.string,
addProductToCart: PropType.func,
inCart: PropType.Boolean | PropType.undefined,
inCart: PropType.bool,
};
export default ProductInfo;

+ 7
- 1
components/sort/Sort.jsx Zobrazit soubor

@@ -3,7 +3,13 @@ import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
const Sort = ({ sort, handleSortChange }) => {
return (
<>
<FormControl sx={{ width: '200px', paddingRight: '15px' }}>
<FormControl
sx={{
width: '200px',
mb: { xs: '10px', sm: '0px' },
mr: { sm: '10px' },
}}
>
<InputLabel id="sort-label">Sort</InputLabel>
<Select
MenuProps={{

+ 8
- 3
components/tab-panel/TabPanel.jsx Zobrazit soubor

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

const TabPanel = ({ children, value, index, ...other }) => {
@@ -9,10 +8,16 @@ const TabPanel = ({ children, value, index, ...other }) => {
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
style={{ height: '80%' }}
>
{value === index && (
<Box sx={{ p: 3 }}>
<Typography>{children}</Typography>
<Box
display="flex"
flexDirection="column"
alignContent="space-between"
sx={{ pt: 3, pl: 3, width: '100%', height: '100%' }}
>
{children}
</Box>
)}
</div>

+ 28
- 0
hooks/useFetchProductData.js Zobrazit soubor

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

export const useFetchSingleProduct = (customID) => {
return useQuery(
['product', customID],
async () => await getProductData(customID)
);
};

export const useFetchSimilarProducts = (category) => {
return useQuery(
['products', category],
async () => await getProductsByCategory(category),
{
enabled: !!category,
}
);
};

export const useFetchProductsByCategory = (productType) => {
return useQuery(
['filteredProducts', productType],
async () => await getProductsByCategory(productType),
{ enabled: productType === 'Mug' || productType === 'Coffee' }
);
};

+ 21
- 0
hooks/useInfiniteQuery.js Zobrazit soubor

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

export const useInfiniteProducts = (filter) => {
return useInfiniteQuery(
['products'],
async ({ pageParam = 1 }) => await getAllProducts(pageParam),
{
getNextPageParam: (lastPage, pages) => {
const maxPages = Math.ceil(lastPage?.productCount / 9);
const nextPage = pages.length + 1;
if (nextPage <= maxPages) {
return nextPage;
}
},
enabled: filter === 'All' || filter === '',
staleTime: 0,
cacheTime: 0,
}
);
};

+ 2
- 0
pages/_app.js Zobrazit soubor

@@ -4,6 +4,7 @@ import {
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { SessionProvider } from 'next-auth/react';
import { appWithTranslation } from 'next-i18next';
import Head from 'next/head';
@@ -38,6 +39,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
</StorageProvider>
</ThemeProvider>
</SessionProvider>
<ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools>
</Hydrate>
</QueryClientProvider>
);

+ 199
- 0
pages/products/[customId].js Zobrazit soubor

@@ -0,0 +1,199 @@
import { Button, Grid, Tab, Tabs, Typography } from '@mui/material';
import { Box, Container } from '@mui/system';
import { dehydrate, QueryClient } from '@tanstack/react-query';
import Image from 'next/image';
import { useRouter } from 'next/router';
import React, { useState } from 'react';
import Loader from '../../components/loader/Loader';
import ProductCard from '../../components/product-card/ProductCard';
import TabPanel from '../../components/tab-panel/TabPanel';
import {
useFetchSimilarProducts,
useFetchSingleProduct,
} from '../../hooks/useFetchProductData';
import { getProductData } from '../../requests/products/producDataRequest';
import { useStore, useStoreUpdate } from '../../store/cart-context';
import { shuffle } from '../../utils/helpers/shuffle';

const SingleProduct = () => {
const { addCartValue } = useStoreUpdate();
const { cartStorage } = useStore();

const router = useRouter();

const { customId } = router.query;

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

const productCategory = data?.product.category;

const { data: similarProducts, isLoading: similarLoading } =
useFetchSimilarProducts(productCategory);

const [value, setValue] = useState(0);

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

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

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

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

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

const productsToShow = (id) => {
const filtered = shuffle(similarProducts?.productsByCategory)
.filter((product) => product.customID !== id)
.slice(0, 3)
.map((item) => (
<Grid
key={item._id}
item
lg={4}
md={6}
sm={6}
xs={12}
sx={{ mb: '100px' }}
>
<ProductCard product={item} />
</Grid>
));

return filtered;
};

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 item md={6} sm={12}>
<Image
src="/images/product-card-image.jpg"
alt="product"
width={900}
height={600}
/>
</Grid>
<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="Purchase"
{...a11yProps(0)}
/>
<Tab sx={{ width: '50%' }} label="Category" {...a11yProps(1)} />
</Tabs>
<TabPanel value={value} index={0}>
<Box flexGrow={2} sx={{ pb: { xs: '70px' } }}>
<Typography>{data.product.description}</Typography>
</Box>
<Box
sx={{
display: { xs: 'flex' },
flexDirection: { xs: 'column' },
justifyContent: { xs: 'center' },
alignItems: { xs: 'center', md: 'flex-end' },
}}
>
<Typography mb={2}>${data.product.price}</Typography>
<Button
disabled={inCart}
onClick={() => addProductToCart(1)}
sx={{
backgroundColor: '#CBA213',
height: 50,
width: { xs: '300px', md: '150px' },
color: 'white',
}}
>
{inCart ? 'In Cart' : 'Add to cart'}
</Button>
</Box>
</TabPanel>
<TabPanel value={value} index={1}>
<Box sx={{ mb: { xs: '60px' } }}>{data.product.category}</Box>
</TabPanel>
</Grid>
</Grid>

<Typography
sx={{
mt: { xs: '60px', md: '100px', lg: '150px' },
mb: 5,
color: 'primary.main',
fontSize: '32px',
}}
>
Other Product You May Like
</Typography>
<Grid container spacing={2}>
{productsToShow(customId)}
</Grid>
</Container>
</Box>
);
};

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

const queryClient = new QueryClient();

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

return {
props: {
dehydratatedState: dehydrate(queryClient),
},
};
};

export default SingleProduct;

+ 0
- 119
pages/products/[slug].js Zobrazit soubor

@@ -1,119 +0,0 @@
import { Grid, Tab, Tabs, Typography } from '@mui/material';
import { Box, Container } from '@mui/system';
import Image from 'next/image';
import React, { useState } from 'react';
import ProductCard from '../../components/product-card/ProductCard';
import TabPanel from '../../components/tab-panel/TabPanel';

const SingleProduct = () => {
const [value, setValue] = useState(0);

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

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

return (
<Box
sx={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<Container
sx={{
width: '1273px',
}}
>
<Typography
fontFamily={'body1.fontFamily'}
fontSize="32px"
sx={{ mt: 25, height: '100%', color: 'primary.main' }}
>
Minimalist Printed Mug
</Typography>
<Grid container spacing={2} sx={{ height: '100%', width: '100%' }}>
<Grid item lg={6}>
<Image
src="/images/product-card-image.jpg"
alt="product"
width={630}
height={390}
/>
</Grid>
<Grid item lg={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="Purchase"
{...a11yProps(0)}
/>
<Tab sx={{ width: '50%' }} label="Category" {...a11yProps(1)} />
</Tabs>
<TabPanel value={value} index={0}>
<Box display="flex" flexDirection="row" justifyContent="right">
<Box>
<Typography>
Our simple and sturdy mugs are made to last. With a
minimalist desings you will soon be enjoying your next brew.
</Typography>
</Box>
<Box
justifyContent="flex-end"
sx={{ display: 'flex', flexDirection: 'column' }}
>
<Typography align="right">$20</Typography>
</Box>
</Box>
</TabPanel>
<TabPanel value={value} index={1}>
Mugs & Cups
</TabPanel>
</Grid>
</Grid>

<Typography
sx={{ mt: 25, mb: 5, color: 'primary.main', fontSize: '32px' }}
>
Other Product You May Like
</Typography>
<Grid container spacing={2} sx={{ height: '100%', width: '100%' }}>
<Grid item lg={4}>
<ProductCard />
</Grid>
<Grid item lg={4}>
<ProductCard />
</Grid>
<Grid item lg={4}>
<ProductCard />
</Grid>
</Grid>
</Container>
</Box>
);
};

export default SingleProduct;

+ 49
- 22
pages/products/index.js Zobrazit soubor

@@ -1,32 +1,59 @@
import { Box } from '@mui/system';
import Head from 'next/head';
import { useState } from 'react';
import FilterSort from '../../components/filter-sort/FilterSort';
import Loader from '../../components/loader/Loader';
import ProductsGrid from '../../components/products-grid/ProductsGrid';
import ProductsHero from '../../components/products-hero/ProductsHero';
import { useInfiniteProducts } from '../../hooks/useInfiniteQuery';

const Products = () => {
return (
<>
<Box sx={{ width: '100%', height: '100%' }}>
<Head>
<title>NextJS template</title>
<meta name="description" content="Random data with pagination..." />
</Head>
<ProductsHero />
</Box>
</>
);
};
const [filter, setFilter] = useState('');
const [sort, setSort] = useState('');
const { data, isLoading, fetchNextPage, hasNextPage } =
useInfiniteProducts(filter);

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

// export async function getStaticProps({ locale }) {
// const queryClient = new QueryClient();
const handleSortChange = (event) => {
const sort = event.target.value;
setSort(sort);
};

// await queryClient.prefetchQuery(['randomData', 1], () => getData(1));
if (isLoading) {
return <Loader loading={isLoading} />;
}

// return {
// props: {
// dehydratedState: dehydrate(queryClient),
// ...(await serverSideTranslations(locale, ['pagination'])),
// },
// };
// }
return (
<Box
sx={{
display: 'flex',
flexDirection: 'column',
}}
>
<Head>
<title>NextJS template</title>
<meta name="description" content="Random data with pagination..." />
</Head>
<ProductsHero />
<FilterSort
handleProductTypeChange={handleProductTypeChange}
productType={filter}
sort={sort}
handleSortChange={handleSortChange}
/>
<ProductsGrid
allProducts={data}
sort={sort}
productType={filter}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
/>
</Box>
);
};

export default Products;

+ 10
- 0
utils/helpers/shuffle.js Zobrazit soubor

@@ -0,0 +1,10 @@
export const shuffle = (array) => {
const newArray = [...array];

newArray.reverse().forEach((item, index) => {
const j = Math.floor(Math.random() * (index + 1));
[newArray[index], newArray[j]] = [newArray[j], newArray[index]];
});

return newArray;
};

+ 19
- 10
utils/helpers/sortHelpers.js Zobrazit soubor

@@ -1,11 +1,20 @@
export const compare = (a, b, sort) => {
if (sort === 'asc') {
if (a > b) return 1;
else if (b > a) return -1;
return 0;
} else if (sort === 'desc') {
if (a > b) return -1;
else if (b > a) return 1;
return 0;
}
/* eslint-disable no-prototype-builtins */
export const compare = (key, order = 'asc') => {
return function innerSort(a, b) {
if (!a.hasOwnProperty(key) || !b.hasOwnProperty(key)) {
// property doesn't exist on either object
return 0;
}

const varA = typeof a[key] === 'string' ? a[key].toUpperCase() : a[key];
const varB = typeof b[key] === 'string' ? b[key].toUpperCase() : b[key];

let comparison = 0;
if (varA > varB) {
comparison = 1;
} else if (varA < varB) {
comparison = -1;
}
return order === 'desc' ? comparison * -1 : comparison;
};
};

Načítá se…
Zrušit
Uložit