Ver código fonte

done redux setup

master
azri 3 dias atrás
pai
commit
8eaae83328

+ 71
- 15
package-lock.json Ver arquivo

@@ -13,6 +13,7 @@
13 13
         "@fontsource/roboto": "^5.1.0",
14 14
         "@mui/icons-material": "^6.1.10",
15 15
         "@mui/material": "^6.1.10",
16
+        "@reduxjs/toolkit": "^2.5.0",
16 17
         "@shopify/shopify-api": "^11.6.0",
17 18
         "@shopify/storefront-api-client": "^1.0.3",
18 19
         "@testing-library/jest-dom": "^5.17.0",
@@ -22,7 +23,7 @@
22 23
         "dotenv": "^16.4.7",
23 24
         "react": "^18.3.1",
24 25
         "react-dom": "^18.3.1",
25
-        "react-redux": "^9.1.2",
26
+        "react-redux": "^9.2.0",
26 27
         "react-router-dom": "^7.0.2",
27 28
         "react-scripts": "5.0.1",
28 29
         "shopify-api-node": "^3.14.0",
@@ -3569,6 +3570,40 @@
3569 3570
         "url": "https://opencollective.com/popperjs"
3570 3571
       }
3571 3572
     },
3573
+    "node_modules/@reduxjs/toolkit": {
3574
+      "version": "2.5.0",
3575
+      "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz",
3576
+      "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==",
3577
+      "license": "MIT",
3578
+      "dependencies": {
3579
+        "immer": "^10.0.3",
3580
+        "redux": "^5.0.1",
3581
+        "redux-thunk": "^3.1.0",
3582
+        "reselect": "^5.1.0"
3583
+      },
3584
+      "peerDependencies": {
3585
+        "react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
3586
+        "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
3587
+      },
3588
+      "peerDependenciesMeta": {
3589
+        "react": {
3590
+          "optional": true
3591
+        },
3592
+        "react-redux": {
3593
+          "optional": true
3594
+        }
3595
+      }
3596
+    },
3597
+    "node_modules/@reduxjs/toolkit/node_modules/immer": {
3598
+      "version": "10.1.1",
3599
+      "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
3600
+      "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
3601
+      "license": "MIT",
3602
+      "funding": {
3603
+        "type": "opencollective",
3604
+        "url": "https://opencollective.com/immer"
3605
+      }
3606
+    },
3572 3607
     "node_modules/@rollup/plugin-babel": {
3573 3608
       "version": "5.3.1",
3574 3609
       "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -4763,9 +4798,9 @@
4763 4798
       "license": "MIT"
4764 4799
     },
4765 4800
     "node_modules/@types/use-sync-external-store": {
4766
-      "version": "0.0.3",
4767
-      "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
4768
-      "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
4801
+      "version": "0.0.6",
4802
+      "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
4803
+      "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
4769 4804
       "license": "MIT"
4770 4805
     },
4771 4806
     "node_modules/@types/ws": {
@@ -15155,17 +15190,17 @@
15155 15190
       "license": "MIT"
15156 15191
     },
15157 15192
     "node_modules/react-redux": {
15158
-      "version": "9.1.2",
15159
-      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz",
15160
-      "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==",
15193
+      "version": "9.2.0",
15194
+      "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
15195
+      "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
15161 15196
       "license": "MIT",
15162 15197
       "dependencies": {
15163
-        "@types/use-sync-external-store": "^0.0.3",
15164
-        "use-sync-external-store": "^1.0.0"
15198
+        "@types/use-sync-external-store": "^0.0.6",
15199
+        "use-sync-external-store": "^1.4.0"
15165 15200
       },
15166 15201
       "peerDependencies": {
15167
-        "@types/react": "^18.2.25",
15168
-        "react": "^18.0",
15202
+        "@types/react": "^18.2.25 || ^19",
15203
+        "react": "^18.0 || ^19",
15169 15204
         "redux": "^5.0.0"
15170 15205
       },
15171 15206
       "peerDependenciesMeta": {
@@ -15393,6 +15428,21 @@
15393 15428
         "node": ">=8"
15394 15429
       }
15395 15430
     },
15431
+    "node_modules/redux": {
15432
+      "version": "5.0.1",
15433
+      "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
15434
+      "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
15435
+      "license": "MIT"
15436
+    },
15437
+    "node_modules/redux-thunk": {
15438
+      "version": "3.1.0",
15439
+      "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
15440
+      "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
15441
+      "license": "MIT",
15442
+      "peerDependencies": {
15443
+        "redux": "^5.0.0"
15444
+      }
15445
+    },
15396 15446
     "node_modules/reflect.getprototypeof": {
15397 15447
       "version": "1.0.7",
15398 15448
       "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.7.tgz",
@@ -15552,6 +15602,12 @@
15552 15602
       "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
15553 15603
       "license": "MIT"
15554 15604
     },
15605
+    "node_modules/reselect": {
15606
+      "version": "5.1.1",
15607
+      "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
15608
+      "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
15609
+      "license": "MIT"
15610
+    },
15555 15611
     "node_modules/resolve": {
15556 15612
       "version": "1.22.8",
15557 15613
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -17841,12 +17897,12 @@
17841 17897
       }
17842 17898
     },
17843 17899
     "node_modules/use-sync-external-store": {
17844
-      "version": "1.2.2",
17845
-      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
17846
-      "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
17900
+      "version": "1.4.0",
17901
+      "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
17902
+      "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==",
17847 17903
       "license": "MIT",
17848 17904
       "peerDependencies": {
17849
-        "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
17905
+        "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
17850 17906
       }
17851 17907
     },
17852 17908
     "node_modules/util-deprecate": {

+ 2
- 1
package.json Ver arquivo

@@ -8,6 +8,7 @@
8 8
     "@fontsource/roboto": "^5.1.0",
9 9
     "@mui/icons-material": "^6.1.10",
10 10
     "@mui/material": "^6.1.10",
11
+    "@reduxjs/toolkit": "^2.5.0",
11 12
     "@shopify/shopify-api": "^11.6.0",
12 13
     "@shopify/storefront-api-client": "^1.0.3",
13 14
     "@testing-library/jest-dom": "^5.17.0",
@@ -17,7 +18,7 @@
17 18
     "dotenv": "^16.4.7",
18 19
     "react": "^18.3.1",
19 20
     "react-dom": "^18.3.1",
20
-    "react-redux": "^9.1.2",
21
+    "react-redux": "^9.2.0",
21 22
     "react-router-dom": "^7.0.2",
22 23
     "react-scripts": "5.0.1",
23 24
     "shopify-api-node": "^3.14.0",

+ 17
- 13
src/components/ImageView/ImageView.jsx Ver arquivo

@@ -1,4 +1,5 @@
1
-import React, { useState } from "react";
1
+import { useState, useEffect } from "react";
2
+import { useSelector } from "react-redux";
2 3
 import {
3 4
   Box,
4 5
   Typography,
@@ -9,15 +10,17 @@ import {
9 10
 import Grid from '@mui/material/Grid2';
10 11
 
11 12
 const ImageView = () => {
12
-  const [previewImage, setPreviewImage] = useState("https://business.aducktive.com/storage/product/23/ceGt6VWGAdW0tVUArAAkpAwk4DUu6QdGA95udBQK.webp"); // Initial preview image
13
+
14
+  const product = useSelector((state) => state.products.product.data)
15
+  
16
+  const [previewImage, setPreviewImage] = useState("");
13 17
   const [isZoomed, setIsZoomed] = useState(false);
14 18
 
15
-  const thumbnails = [
16
-    "https://business.aducktive.com/storage/product/23/ceGt6VWGAdW0tVUArAAkpAwk4DUu6QdGA95udBQK.webp",
17
-    "https://business.aducktive.com/storage/product/24/z6wjsGoGb5X5F72TIW9JihYQR3ZnwEHFgcnfOCOF.webp",
18
-    "https://business.aducktive.com/storage/product/22/JtniBtfevbtO9iJy3LUozgyGGe2HLv2rVNZ7C16e.webp",
19
-    "https://business.aducktive.com/storage/product/25/GTyY9k0PvQUuC4cq5rUNgbIAw6H4vR6S5Ht3PSYn.webp",
20
-  ];
19
+  useEffect(()=>{
20
+
21
+    setPreviewImage(product?.images?.nodes[0]?.url)
22
+  
23
+  },[product])
21 24
 
22 25
   const handleThumbnailClick = (image) => {
23 26
     setPreviewImage(image);
@@ -43,9 +46,10 @@ const ImageView = () => {
43 46
               src={previewImage}
44 47
               alt="Preview"
45 48
               style={{
46
-                width: "100%",
49
+                width: "70%",
47 50
                 height: "auto",
48 51
                 display: "block",
52
+                margin:"auto auto"
49 53
               }}
50 54
             />
51 55
           </Box>
@@ -62,18 +66,18 @@ const ImageView = () => {
62 66
               gap:2
63 67
             }}
64 68
           >
65
-            {thumbnails.map((thumbnail, index) => (
69
+            {product?.images?.nodes?.map(({src, url}, index) => (
66 70
               <IconButton
67 71
                 key={index}
68
-                onClick={() => handleThumbnailClick(thumbnail)}
72
+                onClick={() => handleThumbnailClick(url)}
69 73
                 sx={{
70 74
                   borderRadius: 2,
71 75
                   overflow: "hidden",
72 76
                 }}
73 77
               >
74 78
                 <img
75
-                  src={thumbnail}
76
-                  alt={`Thumbnail ${index + 1}`}
79
+                  src={url}
80
+                  alt={`img_${index + 1}`}
77 81
                   style={{
78 82
                     width: "100%",
79 83
                     height: "auto",

+ 103
- 73
src/components/ProductDetails/ProductDetails.jsx Ver arquivo

@@ -1,106 +1,136 @@
1
-import React from "react";
1
+import { useState } from "react";
2
+import { useSelector } from "react-redux";
2 3
 import {
3 4
   Box,
4 5
   Typography,
5
-  Select,
6
-  MenuItem,
7 6
   Button,
8
-  FormControl,
9
-  InputLabel,
7
+  TextField
10 8
 } from "@mui/material";
11
-import Grid from '@mui/material/Grid2';
9
+import AddIcon from '@mui/icons-material/Add';
10
+import RemoveIcon from '@mui/icons-material/Remove';
12 11
 
13 12
 const ProductDetails = () => {
13
+
14
+  const product = useSelector((state) => state.products.product.data)
15
+  const [quantity, setQuantity] = useState(1);
16
+  const [selectedSize, setSelectedSize] = useState(null);
17
+
18
+  const handleIncrement = () => {
19
+    setQuantity((prevQuantity) => prevQuantity + 1);
20
+  };
21
+
22
+  const handleDecrement = () => {
23
+    setQuantity((prevQuantity) => (prevQuantity > 1 ? prevQuantity - 1 : 1));
24
+  };
25
+
26
+  const handleSizeClick = (size) => {
27
+    setSelectedSize(size);
28
+    // alert(`Selected size: ${size}`);
29
+  };
30
+
14 31
   return (
15
-    <Box sx={{ position: "relative"}}>
32
+    <Box sx={{ position: "relative" }}>
16 33
       {/* Flex Container */}
17
-      <Box sx={{ display: "flex", flexDirection: "column", gap: 2}}>
34
+      <Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
18 35
         {/* Section 1: Product Info */}
19 36
         <Box>
20
-          <Typography variant="h5" sx={{fontWeight:"bold", mb:2}}>Product Name</Typography>
37
+          <Typography variant="h5" sx={{ fontWeight: "bold", mb: 2 }}>
38
+            {product.title}
39
+          </Typography>
21 40
           <Typography variant="body2" color="text.secondary">
22
-            Product Type
41
+            {product?.collections?.nodes[0]?.title}
23 42
           </Typography>
24 43
           <Typography
25 44
             variant="body1"
26 45
             sx={{ fontWeight: "bold" }}
27 46
           >
28
-            $99.99
47
+            {`${product?.compareAtPriceRange?.minVariantPrice?.currencyCode} 
48
+            ${parseFloat(product?.compareAtPriceRange?.minVariantPrice?.amount).toFixed(2)}`}
29 49
           </Typography>
30 50
         </Box>
31 51
 
32
-        {/* Section 2: Description */}
33
-        <Box sx={{mb:5}}>
34
-          <Typography variant="h6" color="text.secondary" sx={{fontWeight:"bold", mb:3}}>
35
-            Description
36
-          </Typography>
37
-          <Typography variant="body1" color="text.secondary" sx={{fontWeight:"400", mb:3 }}>
38
-            Materials : Plain Soft Crepe with embroidery.
39
-          </Typography>
40
-          <Typography variant="body1" color="text.secondary" sx={{fontWeight:"400" }}>
41
-            Care Instruction : Handwash for better care - Machine wash cotton items in cold or warm water. Do not use hot water as it may shrink the fabric - Use normal wash cycle and gentle detergent - Avoid using any harsh chemicals like bleach - Tumble dry on low setting, then promptly remove the item from the dryer to avoid wrinkles - Iron to remove wrinkle. Adding a spray starch will revive the crispness of cotton fabrics. Note : - Sale items are not returnable, exchangeable & refundable - Change of mind not applicable - Wrong washing, storing or ironing method which may cause product damage will not be entertained - Due to lighting effects, monitor’s brightness, contrast setting, etc., they could be some slight differences in the colour tone of the pictures and the actual item. Delivery : Postage will take around 3-5 working days from purchasing date
52
+        {/* Section 2: Size */}
53
+        <Box>
54
+          <Typography variant="body1" sx={{ fontWeight: "bold", color: "#000", mb: 2 }}>
55
+            Size
42 56
           </Typography>
57
+          <Box display="flex" gap={2}>
58
+            {["S", "M", "L", "XL"].map((size) => (
59
+              <Button
60
+                key={size}
61
+                variant={selectedSize === size ? "contained" : "outlined"}
62
+                color={selectedSize === size ? "primary" : "primary"}
63
+                sx={{ color: "#000" }}
64
+                onClick={() => handleSizeClick(size)}
65
+              >
66
+                {size}
67
+              </Button>
68
+            ))}
69
+          </Box>
43 70
         </Box>
44 71
 
45
-        {/* Section 3: Controls */}
46
-        <Grid container spacing={2} sx={{mt:"auto"}}>
47
-          {/* Size Select */}
48
-          <Grid item size={12}>
49
-            <FormControl fullWidth>
50
-              <InputLabel>Size</InputLabel>
51
-              <Select defaultValue="">
52
-                <MenuItem value="small">Small</MenuItem>
53
-                <MenuItem value="medium">Medium</MenuItem>
54
-                <MenuItem value="large">Large</MenuItem>
55
-              </Select>
56
-            </FormControl>
57
-          </Grid>
58 72
 
59
-          {/* Color Select */}
60
-          <Grid item size={12}>
61
-            <FormControl fullWidth>
62
-              <InputLabel>Color</InputLabel>
63
-              <Select defaultValue="">
64
-                <MenuItem value="red">Red</MenuItem>
65
-                <MenuItem value="blue">Blue</MenuItem>
66
-                <MenuItem value="green">Green</MenuItem>
67
-              </Select>
68
-            </FormControl>
69
-          </Grid>
73
+        {/* Section 3: Quantity */}
74
+        <Box sx={{ mb: 5 }}>
75
+          <Typography variant="body1" sx={{ fontWeight: "bold", color: "#000", mb: 2 }}>
76
+            Qunatity
77
+          </Typography>
78
+          <Box display="flex" alignItems="center" gap={2}>
79
+            <Button
80
+              variant="contained"
81
+              color="primary"
82
+              sx={{width:"35px"}}
83
+              onClick={handleDecrement}
84
+            >
85
+              <RemoveIcon />
86
+            </Button>
70 87
 
71
-          {/* Material Select */}
72
-          <Grid item size={12}>
73
-            <FormControl fullWidth>
74
-              <InputLabel>Material</InputLabel>
75
-              <Select defaultValue="">
76
-                <MenuItem value="cotton">Cotton</MenuItem>
77
-                <MenuItem value="wool">Wool</MenuItem>
78
-                <MenuItem value="silk">Silk</MenuItem>
79
-              </Select>
80
-            </FormControl>
81
-          </Grid>
88
+            <TextField
89
+              value={quantity}
90
+              inputProps={{ readOnly: true, style: { textAlign: 'center' } }}
91
+              size="small"
92
+              sx={{width:"100px"}}
93
+              variant="outlined"
94
+            />
82 95
 
83
-          {/* Buy Now Button */}
84
-          <Grid item size={12}>
85 96
             <Button
86
-              onClick={()=>{window.location.href ="/cart"}}
87 97
               variant="contained"
88
-              color="common.black"
89
-              fullWidth
90
-              sx={{
91
-                backgroundColor: (theme) => theme.palette.common.black,
92
-                color: "white",
93
-                textTransform: "none",
94
-                mt: 2,
95
-                "&:hover": {
96
-                  backgroundColor: (theme) => theme.palette.grey[900],
97
-                },
98
-              }}
98
+              color="primary"
99
+              sx={{width:"35px"}}
100
+              onClick={handleIncrement}
99 101
             >
100
-              BUY NOW
102
+              <AddIcon />
101 103
             </Button>
102
-          </Grid>
103
-        </Grid>
104
+          </Box>
105
+        </Box>
106
+
107
+        {/* Section 4: Description */}
108
+        <Box sx={{ mb: 5 }}>
109
+          <Typography variant="body1" sx={{ fontWeight: "bold", color: "#000" }}>
110
+            Description
111
+          </Typography>
112
+          <Typography variant="body1" color="text.secondary" sx={{ fontWeight: "400" }}>
113
+            <div dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}></div>
114
+          </Typography>
115
+        </Box>
116
+
117
+        <Button
118
+          onClick={() => { window.location.href = "/cart" }}
119
+          variant="contained"
120
+          color="common.black"
121
+          fullWidth
122
+          sx={{
123
+            backgroundColor: (theme) => theme.palette.common.black,
124
+            color: "white",
125
+            textTransform: "none",
126
+            mt: 2,
127
+            "&:hover": {
128
+              backgroundColor: (theme) => theme.palette.grey[900],
129
+            },
130
+          }}
131
+        >
132
+          BUY NOW
133
+        </Button>
104 134
       </Box>
105 135
 
106 136
     </Box>

+ 73
- 93
src/components/ProductList/ProductList.jsx Ver arquivo

@@ -1,107 +1,87 @@
1
-import React from 'react';
1
+import React, { useEffect } from 'react';
2 2
 import { Box, Typography, Button } from '@mui/material';
3 3
 import Grid from '@mui/material/Grid2';
4 4
 
5
+//REDUX
6
+import { useSelector, useDispatch } from 'react-redux';
7
+import { fetchProducts } from '../../redux/slices/productSlice';
8
+
5 9
 const ProductList = ({ size = 99999 }) => {
6 10
 
7
-  const images = [
8
-    {
9
-      id: 1,
10
-      url: 'https://via.placeholder.com/300',
11
-      title: 'KEMBOJA IN BEIGE',
12
-      price: 123.90,
13
-      description: "Atma Sari Kurta Raya Collection 2024",
14
-      extra_desc: "or 3 payments of 68.50 MYR with Atome"
15
-    },
16
-    {
17
-      id: 2,
18
-      url: 'https://via.placeholder.com/300',
19
-      title: 'KEMBOJA IN BEIGE',
20
-      price: 123.90,
21
-      description: "Atma Sari Kurta Raya Collection 2024",
22
-      extra_desc: "or 3 payments of 68.50 MYR with Atome"
23
-    },
24
-    {
25
-      id: 3,
26
-      url: 'https://via.placeholder.com/300',
27
-      title: 'KEMBOJA IN BEIGE',
28
-      price: 123.90,
29
-      description: "Atma Sari Kurta Raya Collection 2024",
30
-      extra_desc: "or 3 payments of 68.50 MYR with Atome"
31
-    },
32
-    {
33
-      id: 4,
34
-      url: 'https://via.placeholder.com/300',
35
-      title: 'KEMBOJA IN BEIGE',
36
-      price: 123.90,
37
-      description: "Atma Sari Kurta Raya Collection 2024",
38
-      extra_desc: "or 3 payments of 68.50 MYR with Atome"
39
-    },
40
-    {
41
-      id: 5,
42
-      url: 'https://via.placeholder.com/300',
43
-      title: 'KEMBOJA IN BEIGE',
44
-      price: 123.90,
45
-      description: "Atma Sari Kurta Raya Collection 2024",
46
-      extra_desc: "or 3 payments of 68.50 MYR with Atome"
47
-    },
48
-    {
49
-      id: 6,
50
-      url: 'https://via.placeholder.com/300',
51
-      title: 'KEMBOJA IN BEIGE',
52
-      price: 123.90,
53
-      description: "Atma Sari Kurta Raya Collection 2024",
54
-      extra_desc: "or 3 payments of 68.50 MYR with Atome"
55
-    }
56
-  ];
11
+  const products = useSelector((state) => state.products.products.data)
12
+  const dispatch = useDispatch();
13
+
14
+  useEffect(() => {
15
+
16
+    dispatch(fetchProducts())
17
+
18
+  }, [])
19
+
20
+  const renderProduct = (id, img_url, title, collection, price, currency, extra_desc) => {
21
+
22
+    return (
23
+      <Grid item size={{ xs: 6, sm: 6, md: 3 }}>
24
+        <Box
25
+          onClick={() => { window.location.href = `/products/${id}` }}
26
+          sx={{
27
+            overflow: 'hidden',
28
+            position: 'relative',
29
+            cursor: 'pointer'
30
+          }}
31
+
32
+        >
33
+          <img
34
+            src={img_url}
35
+            alt={title}
36
+            style={{
37
+              width: '100%',
38
+              aspectRatio: '3 / 4',
39
+              objectFit: 'cover',
40
+            }}
41
+          />
42
+
43
+          <Button sx={{ position: "absolute", top: 20, left: 20, boxShadow: 0 }} variant="contained">
44
+            NEW
45
+          </Button>
46
+
47
+          <Box sx={{ py: 5 }}>
48
+            <Typography variant="body2" >
49
+              {collection}
50
+            </Typography>
51
+            <Typography variant="h5" sx={{ fontWeight: "bolder" }}>
52
+              {title}
53
+            </Typography>
54
+            <Typography variant="body" >
55
+              {`${currency} ${price}`}
56
+            </Typography>
57
+            <Typography variant="body2" sx={{ mt: 2 }}>
58
+              {extra_desc}
59
+            </Typography>
60
+          </Box>
61
+        </Box>
62
+      </Grid>
63
+    )
64
+
65
+  }
57 66
 
58 67
   return (
59 68
     <Box sx={{ mb: 5 }}>
60 69
       <Grid container spacing={1} columns={12}>
61
-        {images.map((image, index) => {
70
+        {products.map((product, index) => {
71
+          
72
+          let {id, title, compareAtPriceRange, images, collections} = product
73
+          let price = compareAtPriceRange.maxVariantPrice.amount
74
+          let currency = compareAtPriceRange.maxVariantPrice.currencyCode
75
+          let img_url = images.nodes[0].url
76
+          let collection_name = collections.nodes[0]?.title
77
+          
78
+          // ID
79
+          const parts = id.split('/');
80
+          let prodID = parts[parts.length - 1];
62 81
 
63
-          if (index < size) {
64 82
 
65
-            return <Grid item size={{xs:6, sm:6, md:3}} key={index}>
66
-              <Box
67
-                onClick={()=>{ window.location.href = `/products/${image.id}` }}
68
-                sx={{
69
-                  overflow: 'hidden',
70
-                  position: 'relative',
71
-                  cursor: 'pointer'
72
-                }}
73
-        
74
-              >
75
-                <img
76
-                  src={image.url}
77
-                  alt={image.title}
78
-                  style={{
79
-                    width: '100%',
80
-                    aspectRatio: '3 / 4',
81
-                    objectFit: 'cover',
82
-                  }}
83
-                />
84
-
85
-                <Button sx={{ position: "absolute", top: 20, left: 20, boxShadow: 0 }} variant="contained">
86
-                  NEW
87
-                </Button>
88
-
89
-                <Box sx={{ py: 5 }}>
90
-                  <Typography variant="body2" >
91
-                    {image.description}
92
-                  </Typography>
93
-                  <Typography variant="h5" sx={{ fontWeight: "bolder" }}>
94
-                    {image.title}
95
-                  </Typography>
96
-                  <Typography variant="body" >
97
-                    {`RM ${image.price}`}
98
-                  </Typography>
99
-                  <Typography variant="body2" sx={{ mt: 2 }}>
100
-                    {image.extra_desc}
101
-                  </Typography>
102
-                </Box>
103
-              </Box>
104
-            </Grid>
83
+          if (index < size) {
84
+            return renderProduct(prodID, img_url, title, collection_name, price, currency, "" )
105 85
           }
106 86
 
107 87
         })}

+ 5
- 1
src/index.js Ver arquivo

@@ -2,6 +2,8 @@ import React from 'react';
2 2
 import ReactDOM from 'react-dom/client';
3 3
 import './index.css';
4 4
 import App from './App';
5
+import { Provider } from 'react-redux';
6
+import store from './redux/store';
5 7
 import '@fontsource/roboto/300.css';
6 8
 import '@fontsource/roboto/400.css';
7 9
 import '@fontsource/roboto/500.css';
@@ -12,7 +14,9 @@ import '@fontsource/roboto/700.css';
12 14
 const root = ReactDOM.createRoot(document.getElementById('root'));
13 15
 root.render(
14 16
   <React.StrictMode>
15
-    <App />
17
+    <Provider store={store}>
18
+      <App />
19
+    </Provider>
16 20
   </React.StrictMode>
17 21
 );
18 22
 

+ 6
- 0
src/pages/Products/Product.jsx Ver arquivo

@@ -1,6 +1,7 @@
1 1
 import React, { useEffect } from 'react'
2 2
 import { Box, Typography } from '@mui/material';
3 3
 import { useParams } from 'react-router-dom';
4
+import { useSelector, useDispatch } from 'react-redux';
4 5
 import Grid from '@mui/material/Grid2';
5 6
 import logoSrc from "../../assets/svg/logo.svg";
6 7
 import ImageView from '../../components/ImageView/ImageView';
@@ -9,13 +10,18 @@ import ProductSuggestion from '../../components/ProductSuggestion';
9 10
 import ProductHistoryList from '../../components/ProductHistoryList';
10 11
 import SocialMedia from '../../components/SocialMedia'
11 12
 import Feature from '../../components/Feature'
13
+import { fetchProduct } from '../../redux/slices/productSlice';
12 14
 
13 15
 const Product = () => {
14 16
 
15 17
   let { pid } = useParams();
18
+  const product = useSelector((state) => state.products.product.data)
19
+  const dispatch = useDispatch();
16 20
 
17 21
   useEffect(() => {
18 22
 
23
+    dispatch(fetchProduct(pid))
24
+
19 25
   }, [])
20 26
 
21 27
   return (

+ 74
- 0
src/redux/slices/productSlice.js Ver arquivo

@@ -0,0 +1,74 @@
1
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
2
+import ProductService from '../../services/ProductService';
3
+
4
+const initialState = {
5
+    product:{
6
+      data: {},
7
+      status: 'idle',
8
+      error: null,
9
+    },
10
+    products:{
11
+      data: [],
12
+      status: 'idle',
13
+      error: null,
14
+    },
15
+    productsHistory:[],
16
+    productsSelected:[],
17
+    ProductsSuggestion:[]
18
+};
19
+
20
+export const fetchProduct = createAsyncThunk(
21
+  'product/fetchProduct',
22
+  async (pid) => {
23
+    
24
+    const response = await ProductService.getProduct(pid)
25
+    return response
26
+
27
+  }
28
+)
29
+
30
+export const fetchProducts = createAsyncThunk(
31
+  'product/fetchProducts',
32
+  async () => {
33
+    const response = await ProductService.getProducts()
34
+    return response
35
+
36
+  }
37
+)
38
+
39
+export const productSlice = createSlice({
40
+  name: 'product',
41
+  initialState,
42
+  extraReducers: (builder) => {
43
+    builder
44
+      .addCase(fetchProducts.pending, (state) => {
45
+        state.products.status = 'loading';
46
+        state.products.error = null;
47
+      })
48
+      .addCase(fetchProducts.fulfilled, (state, action) => {
49
+        state.products.status = 'succeeded';
50
+        state.products.data = action.payload
51
+      })
52
+      .addCase(fetchProducts.rejected, (state, action) => {
53
+        state.products.status = 'failed';
54
+        state.products.data = [];
55
+        state.products.error = action.error.message;
56
+      })
57
+      .addCase(fetchProduct.pending, (state) => {
58
+        state.product.status = 'loading';
59
+        state.product.error = null;
60
+      })
61
+      .addCase(fetchProduct.fulfilled, (state, action) => {
62
+        state.product.status = 'succeeded';
63
+        state.product.data = action.payload
64
+      })
65
+      .addCase(fetchProduct.rejected, (state, action) => {
66
+        state.product.status = 'failed';
67
+        state.product.data = {};
68
+        state.product.error = action.error.message;
69
+      })
70
+  }
71
+});
72
+
73
+
74
+export default productSlice.reducer;

+ 10
- 0
src/redux/store.js Ver arquivo

@@ -0,0 +1,10 @@
1
+import { configureStore } from '@reduxjs/toolkit';
2
+import productsReducer from './slices/productSlice';
3
+
4
+const store = configureStore({
5
+  reducer: {
6
+    products: productsReducer,
7
+  },
8
+});
9
+
10
+export default store;

+ 102
- 23
src/services/ProductService.js Ver arquivo

@@ -1,51 +1,130 @@
1
-import axios from 'axios'
2 1
 import { createStorefrontApiClient } from '@shopify/storefront-api-client';
3
-import { API_URL, REACT_APP_API_KEY, REACT_APP_API_SECRET, REACT_APP_ACCESS_TOKEN, REACT_APP_SHOP_NAME } from '../utils/httpCommon'
2
+import { REACT_APP_ACCESS_TOKEN, REACT_APP_SHOP_NAME } from '../utils/httpCommon'
4 3
 
5
-const shopUrl = REACT_APP_SHOP_NAME;
6
-const accessToken = REACT_APP_ACCESS_TOKEN;
4
+const defaultQuery = 'status:active, product_type:clothing, product_categroy:dresses';
5
+const defaultResult = 10;
6
+const defaultSortBy = 'TITLE';
7 7
 
8 8
 const client = createStorefrontApiClient({
9
-  storeDomain: `https://${shopUrl}/api/2024-10/graphql.json`,
9
+  storeDomain: `https://${REACT_APP_SHOP_NAME}/api/2024-10/graphql.json`,
10 10
   apiVersion: '2024-10',
11
-  publicAccessToken: accessToken,
11
+  publicAccessToken: REACT_APP_ACCESS_TOKEN,
12 12
 });
13 13
 
14
+// getProduct(5, CREATED_AT, 'product_type:clothing'); 
15
+const getProducts = async (maxResults = defaultResult, sortBy = defaultSortBy, customQuery = defaultQuery) => {
14 16
 
15
-const getProducts = async () => {
17
+  const query = `{
18
+    products(first: ${maxResults}, query: "${customQuery}", sortKey: ${sortBy}) {
19
+      nodes {
20
+        id
21
+        title
22
+        productType
23
+        compareAtPriceRange {
24
+          maxVariantPrice {
25
+            amount
26
+            currencyCode
27
+          }
28
+          minVariantPrice {
29
+            amount
30
+            currencyCode
31
+          }
32
+        }
33
+        images(first: 1) {
34
+          nodes {
35
+            src
36
+            url
37
+          }
38
+        }
39
+        collections(first: 1) {
40
+          nodes {
41
+            title
42
+          }
43
+        }
44
+        variants(first: 5) {
45
+          nodes {
46
+            id
47
+            title
48
+            price {
49
+              amount
50
+              currencyCode
51
+            }
52
+          }
53
+        }
54
+      }
55
+      pageInfo {
56
+        hasNextPage
57
+        hasPreviousPage
58
+      }
59
+    }
60
+  }`
61
+
62
+  const { data, errors, extensions } = await client.request(query, {
63
+    variables: {
64
+      handle: 'sample-product',
65
+    },
66
+  });
16 67
 
17
-  const productQuery = `
18
-    {
19
-      products(first: 99) {
68
+  return data.products.nodes
69
+
70
+}
71
+
72
+const getProduct = async (id = "") => {
73
+
74
+  const query = `{
75
+    product(id: "gid://shopify/Product/${id}") {
76
+      id
77
+      title
78
+      descriptionHtml
79
+      productType
80
+      compareAtPriceRange {
81
+        maxVariantPrice {
82
+          currencyCode
83
+          amount
84
+        }
85
+        minVariantPrice {
86
+          amount
87
+          currencyCode
88
+        }
89
+      }
90
+      images(first: 4) {
20 91
         nodes {
21
-          media(first: 4) {
22
-            nodes {
23
-              previewImage {
24
-                url
25
-              }
26
-            }
92
+          src
93
+          url
94
+        }
95
+      }
96
+      variants(first: 5) {
97
+        nodes {
98
+          id
99
+          title
100
+          price {
101
+            amount
102
+            currencyCode
27 103
           }
104
+        }
105
+      }
106
+      collections(first: 1) {
107
+        nodes {
28 108
           title
29
-          tags
30 109
         }
31 110
       }
32 111
     }
33
-  `;
112
+  }`
34 113
 
35
-  const { data, errors, extensions } = await client.request(productQuery, {
114
+  const { data, errors, extensions } = await client.request(query, {
36 115
     variables: {
37 116
       handle: 'sample-product',
38 117
     },
39 118
   });
40 119
 
41
-  console.log(data)
42
-
43
-  return
120
+  return data.product
44 121
 
45 122
 }
46 123
 
124
+
47 125
 const ProductService = {
48
-  getProducts
126
+  getProducts,
127
+  getProduct
49 128
 }
50 129
 
51 130
 export default ProductService

Carregando…
Cancelar
Salvar