Amber Shopify Project created using ReactJS+React-Redux with GraphQL API integration. Storefront Shopify API: https://github.com/Shopify/shopify-app-js/tree/main/packages/api-clients/storefront-api-client#readme
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProductList.jsx 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. import { useEffect, useState } from 'react';
  2. import { Box, Typography, Button, FormControl, Select, MenuItem, InputBase } from '@mui/material';
  3. import Grid from '@mui/material/Grid2';
  4. import { styled } from "@mui/material";
  5. //REDUX
  6. import { useSelector, useDispatch } from 'react-redux';
  7. import { fetchProducts } from '../../redux/slices/productSlice';
  8. //UTIL FUNCTION
  9. function getAllTags(data) {
  10. const products = data || [];
  11. const allTags = products.flatMap(product => product.tags);
  12. const uniqueTags = [...new Set(allTags)];
  13. return uniqueTags;
  14. }
  15. function getAllCollection(data) {
  16. const products = data || [];
  17. const allCollection = products.flatMap(product => product.collections);
  18. const uniqueCollection = Array.from(
  19. new Map(allCollection.map(item => [item?.title, item])).values()
  20. );
  21. return uniqueCollection;
  22. }
  23. const BootstrapInput = styled(InputBase)(({ theme }) => ({
  24. 'label + &': {
  25. marginTop: theme.spacing(3),
  26. },
  27. '& .MuiInputBase-input': {
  28. position: 'relative',
  29. backgroundColor: "#2E2E2E",
  30. border: '1px solid #ced4da',
  31. color: "#FFF",
  32. fontSize: 13,
  33. padding: '5px 0',
  34. paddingRight: '50px !important',
  35. paddingLeft: "10px",
  36. transition: theme.transitions.create(['border-color', 'box-shadow']),
  37. '&:focus': {
  38. borderRadius: 4,
  39. borderColor: '#80bdff',
  40. boxShadow: '0 0 0 0.2rem rgba(0,123,255,.25)',
  41. },
  42. },
  43. '& .MuiSvgIcon-root': {
  44. color: "#FFF !important"
  45. },
  46. }));
  47. const ProductList = ({ size = 99999 }) => {
  48. const products = useSelector((state) => state.products.products.data) // only used as referenced
  49. const [filteredProducts, setFilteredProducts] = useState([]) // this one is the actual data to be rendered
  50. const [tagFilterOption, setTagFilterOption] = useState([])
  51. const [collectionFilterOption, setCollectionFilterOption] = useState([])
  52. const dispatch = useDispatch();
  53. //filter
  54. const [tags, setTags] = useState('all');
  55. const [collection, setCollection] = useState('all');
  56. const [sort, setSort] = useState('title')
  57. useEffect(() => {
  58. dispatch(fetchProducts())
  59. }, [])
  60. useEffect(() => {
  61. console.log("Products: ", products)
  62. if (products.length > 0) {
  63. let productType = sessionStorage.getItem('amber-select-product-type')
  64. let newFilteredProducts = products.filter(
  65. (product) => product.productType === productType
  66. );
  67. setTimeout(() => {
  68. setFilteredProducts(newFilteredProducts)
  69. }, 100)
  70. const tagList = getAllTags(newFilteredProducts);
  71. setTagFilterOption(tagList);
  72. // Filter will only exist if the user haven't click on collection
  73. if (!sessionStorage.getItem('amber-select-collection')) {
  74. const collectionList = getAllCollection(newFilteredProducts);
  75. setCollectionFilterOption(collectionList);
  76. } else {
  77. const selectedColletion = JSON.parse(sessionStorage.getItem('amber-select-collection'))
  78. setCollection(selectedColletion?.title)
  79. }
  80. }
  81. }, [products])
  82. useEffect(() => {
  83. if (products?.length > 0) {
  84. let productType = sessionStorage.getItem('amber-select-product-type')
  85. let newFilteredProducts = products.filter(
  86. (product) => product.productType === productType
  87. );
  88. // Tags
  89. newFilteredProducts = newFilteredProducts.filter(
  90. (product) => {
  91. if (tags == 'all') {
  92. return product.productType === productType
  93. } else {
  94. return product.productType === productType && product.tags.includes(tags)
  95. }
  96. }
  97. );
  98. // Collection
  99. newFilteredProducts = newFilteredProducts.filter(
  100. (product) => {
  101. if (collection == 'all') {
  102. return product.productType === productType
  103. } else {
  104. return product.productType === productType && product.collections.some(data => data?.title === collection)
  105. }
  106. }
  107. );
  108. if (sort === "title") {
  109. newFilteredProducts = newFilteredProducts.sort((a, b) => a.title.localeCompare(b.title));
  110. } else if (sort === "new") {
  111. newFilteredProducts = newFilteredProducts.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  112. } else if (sort === "old") {
  113. newFilteredProducts = newFilteredProducts.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
  114. }
  115. setFilteredProducts([]) //cause I want those fadeIn animation
  116. setTimeout(() => {
  117. setFilteredProducts(newFilteredProducts)
  118. }, 100)
  119. }
  120. }, [tags, collection, sort])
  121. const handleChange = (event) => {
  122. //setInput({ ...input, [event.target.name]: event.target.value });
  123. };
  124. const renderProduct = (handle, img_url, title, collection_name, minPrice, minPriceCurrency, maxPrice, maxPriceCurrency, extra_desc, selected = false) => {
  125. return (
  126. <Grid className="animate__animated animate__fadeIn" item size={{ xs: 6, sm: 6, md: 3 }}>
  127. <a href={`/products/${handle}`} style={{ textDecoration: "none", color: "#000" }}>
  128. <Box
  129. sx={{
  130. overflow: 'hidden',
  131. position: 'relative',
  132. cursor: 'pointer'
  133. }}
  134. >
  135. <img
  136. src={img_url}
  137. alt={title}
  138. style={{
  139. width: '100%',
  140. aspectRatio: '3 / 4',
  141. objectFit: 'cover',
  142. objectPosition: 'top center'
  143. }}
  144. />
  145. {(selected) && <Button
  146. sx={{
  147. position: "absolute",
  148. top: {
  149. xs: 0,
  150. sm: 0,
  151. md: 10,
  152. lg: 20
  153. },
  154. left: {
  155. xs: 0,
  156. sm: 0,
  157. md: 10,
  158. lg: 20
  159. },
  160. boxShadow: 0,
  161. fontSize: 10
  162. }}
  163. variant="contained">
  164. NEW
  165. </Button>}
  166. <Box sx={{ pb: 1, pt: 1, width: "90%" }}>
  167. <Typography variant="body2" sx={{ fontWeight: "400" }}>
  168. {collection_name}
  169. </Typography>
  170. <Typography variant="body2" sx={{ fontWeight: "bolder"}}>
  171. {title}
  172. </Typography>
  173. <Typography variant="body2" sx={{ fontWeight: "400" }}>
  174. {`${minPriceCurrency} ${parseFloat(minPrice).toFixed(2)}`}
  175. </Typography>
  176. <Typography variant="body2" sx={{ mt: 2 }}>
  177. {extra_desc}
  178. </Typography>
  179. </Box>
  180. </Box>
  181. </a>
  182. </Grid>
  183. )
  184. }
  185. return (
  186. <>
  187. {/* FILTER */}
  188. <Box
  189. sx={{
  190. display: "flex",
  191. justifyContent: "space-between",
  192. py: 0,
  193. flexDirection: {
  194. xs: "column",
  195. sm: "row",
  196. md: "row",
  197. lg: "row"
  198. },
  199. alignItems: "center",
  200. backgroundColor: "background.black",
  201. color: "white",
  202. px: 2, // Add padding around the box
  203. my: 4
  204. }}
  205. >
  206. {/* Left Side: Page Title */}
  207. <Typography variant="body2" sx={{ fontSize: 10, mt: { xs: 1, sm: 1, md: 0 } }}>
  208. {`${filteredProducts.length} Item`}
  209. </Typography>
  210. {/* Right Side: Option Inputs */}
  211. <Box sx={{
  212. display: "flex", gap: 2, flexDirection: "row", flexWrap:"wrap", justifyContent:"space-between", py:{
  213. xs:2,
  214. sm:2,
  215. md:0
  216. }
  217. }}>
  218. {(tagFilterOption.length > 0) && <FormControl sx={{ m: 1, display: "flex", flexDirection: "row" }} variant="standard">
  219. <Typography variant="body2" sx={{ mr: 1, my: "auto" }}>Tag</Typography>
  220. <Select
  221. value={tags}
  222. onChange={(event) => {
  223. setTags(event.target.value);
  224. }}
  225. sx={{
  226. '& .MuiSelect-select': {
  227. border: "none"
  228. }
  229. }}
  230. input={<BootstrapInput />}
  231. name="type"
  232. >
  233. <MenuItem value={'all'}>All</MenuItem>
  234. {tagFilterOption?.map((data) => (<MenuItem value={data}>{data}</MenuItem>))}
  235. </Select>
  236. </FormControl>}
  237. {(collectionFilterOption.length > 0) && <FormControl sx={{ m: 1, display: "flex", flexDirection: "row" }} variant="standard">
  238. <Typography variant="body2" sx={{ mr: 1, my: "auto" }}>Collection</Typography>
  239. <Select
  240. value={collection}
  241. onChange={(event) => {
  242. setCollection(event.target.value);
  243. }}
  244. sx={{
  245. '& .MuiSelect-select': {
  246. border: "none"
  247. }
  248. }}
  249. input={<BootstrapInput />}
  250. name="type"
  251. >
  252. <MenuItem value={'all'}>All</MenuItem>
  253. {collectionFilterOption?.map(({ title }) => (<MenuItem value={title}>{title}</MenuItem>))}
  254. </Select>
  255. </FormControl>}
  256. <FormControl sx={{ m: 1, display: "flex", flexDirection: "row" }} variant="standard">
  257. <Typography variant="body2" sx={{ mr: 1, my: "auto" }}>Sort</Typography>
  258. <Select
  259. value={sort}
  260. onChange={(event) => {
  261. setSort(event.target.value);
  262. }}
  263. sx={{
  264. '& .MuiSelect-select': {
  265. border: "none"
  266. }
  267. }}
  268. input={<BootstrapInput />}
  269. name="sort"
  270. >
  271. <MenuItem defaultValue value={'title'}>Title</MenuItem>
  272. <MenuItem defaultValue value={'new'}>Newest</MenuItem>
  273. <MenuItem defaultValue value={'old'}>Oldest</MenuItem>
  274. </Select>
  275. </FormControl>
  276. </Box>
  277. </Box>
  278. {/* LIST */}
  279. <Box sx={{ mb: 5 }}>
  280. <Grid container spacing={0.5} columns={12}>
  281. {filteredProducts.map((product, index) => {
  282. let { handle, title, images, collections, minVariantPrice, maxVariantPrice, productType, variants, selected } = product
  283. let minPrice = minVariantPrice.amount
  284. let minPriceCurrency = minVariantPrice.currencyCode
  285. let maxPrice = maxVariantPrice.amount
  286. let maxPriceCurrency = maxVariantPrice.currencyCode
  287. let img_url = images[0]?.url
  288. let collection_name = collections[0]?.title
  289. if (index < size) {
  290. return renderProduct(handle, img_url, title, collection_name, minPrice, minPriceCurrency, maxPrice, maxPriceCurrency, "", selected)
  291. }
  292. })}
  293. </Grid>
  294. </Box>
  295. </>
  296. );
  297. };
  298. export default ProductList;