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

Merge branch 'single-product' of ntasicc/coffee into master

pagination
lazarkostic преди 3 години
родител
ревизия
5f7cc4d262

+ 3
- 0
.vscode/settings.json Целия файл

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

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

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

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

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

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

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

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

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

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

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

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

@@ -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';
@@ -50,6 +51,7 @@ function MyApp({ Component, pageProps: { session, ...pageProps } }) {
</Providers>
</ThemeProvider>
</SessionProvider>
<ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools>
</Hydrate>
</QueryClientProvider>
);

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

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

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

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

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

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

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