Quellcode durchsuchen

feat: added product and single product pages

single-product
Lazar Kostic vor 3 Jahren
Ursprung
Commit
30c5dd1ab5

+ 3
- 0
.vscode/settings.json Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

@@ -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 Datei anzeigen

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

Laden…
Abbrechen
Speichern