diff --git a/.gitignore b/.gitignore
index 29dadaf..5d5730a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
**/node_modules
**/.env
**/.husky
-**/.eslintcache
\ No newline at end of file
+**/.eslintcache
+**/dist
\ No newline at end of file
diff --git a/README.md b/README.md
index cad76f6..48af3df 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,17 @@
Back-end repositorie is available [here](https://github.com/SilviaPabon/buenavida-backend).
+## Requirements
+
+1. Running golang API (Back-end repository)
+2. `.env` file created at `src/` with the following variables:
+
+```
+VITE_API_HOST=yourhost
+```
+
+If you are running the project in local environment, then, `VITE_API_HOST`is `localhost:3030`
+
## Avilable node scripts
Run in development / watch mode
diff --git a/index.html b/index.html
index e0d1c84..f5f1d40 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,7 @@
-
Vite + React + TS
+ Buenavida
diff --git a/netlify.toml b/netlify.toml
new file mode 100644
index 0000000..ca22269
--- /dev/null
+++ b/netlify.toml
@@ -0,0 +1,24 @@
+[[redirects]]
+ from = "/"
+ to = "/index.html"
+ status = 200
+
+[[redirects]]
+ from = "/login"
+ to = "/index.html"
+ status = 200
+
+[[redirects]]
+ from = "/signup"
+ to = "/index.html"
+ status = 200
+
+[[redirects]]
+ from = "/*"
+ to = "/index.html"
+ status = 404
+
+[[redirects]]
+ from = "/cart"
+ to = "/index.html"
+ status = 200
diff --git a/package-lock.json b/package-lock.json
index 2488c19..e84f4fc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,13 @@
"name": "buenavida-frontend",
"version": "0.0.0",
"dependencies": {
+ "axios": "^1.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "6.4.2"
+ "react-icons": "4.6.0",
+ "react-paginate": "^8.1.3",
+ "react-router-dom": "6.4.2",
+ "react-toastify": "^9.1.1"
},
"devDependencies": {
"@types/react": "^18.0.17",
@@ -1091,6 +1095,21 @@
"node": ">=8"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/axios": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
+ "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1236,6 +1255,14 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1257,6 +1284,17 @@
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
"dev": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz",
@@ -1337,6 +1375,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -2307,6 +2353,38 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -3315,6 +3393,25 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -3409,7 +3506,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3733,13 +3829,17 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -3792,11 +3892,29 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-icons": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz",
+ "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/react-paginate": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.1.3.tgz",
+ "integrity": "sha512-zBp80DBRcaeBnAeHUfbGKD0XHfbGNUolQ+S60Ymfs8o7rusYaJYZMAt1j93ADDNLlzRmJ0tMF/NeTlcdKf7dlQ==",
+ "dependencies": {
+ "prop-types": "^15.6.1"
+ },
+ "peerDependencies": {
+ "react": "^16 || ^17 || ^18"
+ }
},
"node_modules/react-refresh": {
"version": "0.14.0",
@@ -3837,6 +3955,18 @@
"react-dom": ">=16.8"
}
},
+ "node_modules/react-toastify": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
+ "integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
+ "dependencies": {
+ "clsx": "^1.1.1"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
@@ -5368,6 +5498,21 @@
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "axios": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz",
+ "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==",
+ "requires": {
+ "follow-redirects": "^1.15.0",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -5463,6 +5608,11 @@
"string-width": "^5.0.0"
}
},
+ "clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg=="
+ },
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -5484,6 +5634,14 @@
"integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==",
"dev": true
},
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
"commander": {
"version": "9.4.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-9.4.1.tgz",
@@ -5544,6 +5702,11 @@
"object-keys": "^1.1.1"
}
},
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+ },
"dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -6182,6 +6345,21 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
+ "follow-redirects": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
+ "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
+ },
+ "form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ }
+ },
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6885,6 +7063,19 @@
"picomatch": "^2.3.1"
}
},
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
"mimic-fn": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
@@ -6950,8 +7141,7 @@
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
- "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="
},
"object-inspect": {
"version": "1.12.2",
@@ -7163,13 +7353,17 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@@ -7199,11 +7393,24 @@
"scheduler": "^0.23.0"
}
},
+ "react-icons": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.6.0.tgz",
+ "integrity": "sha512-rR/L9m9340yO8yv1QT1QurxWQvWpbNHqVX0fzMln2HEb9TEIrQRGsqiNFQfiv9/JEUbyHmHPlNTB2LWm2Ttz0g==",
+ "requires": {}
+ },
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
- "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "react-paginate": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.1.3.tgz",
+ "integrity": "sha512-zBp80DBRcaeBnAeHUfbGKD0XHfbGNUolQ+S60Ymfs8o7rusYaJYZMAt1j93ADDNLlzRmJ0tMF/NeTlcdKf7dlQ==",
+ "requires": {
+ "prop-types": "^15.6.1"
+ }
},
"react-refresh": {
"version": "0.14.0",
@@ -7228,6 +7435,14 @@
"react-router": "6.4.2"
}
},
+ "react-toastify": {
+ "version": "9.1.1",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz",
+ "integrity": "sha512-pkFCla1z3ve045qvjEmn2xOJOy4ZciwRXm1oMPULVkELi5aJdHCN/FHnuqXq8IwGDLB7PPk2/J6uP9D8ejuiRw==",
+ "requires": {
+ "clsx": "^1.1.1"
+ }
+ },
"regexp.prototype.flags": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
diff --git a/package.json b/package.json
index fe0c947..7152b78 100644
--- a/package.json
+++ b/package.json
@@ -12,9 +12,13 @@
"lint": "npx eslint ."
},
"dependencies": {
+ "axios": "^1.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-router-dom": "6.4.2"
+ "react-icons": "4.6.0",
+ "react-paginate": "^8.1.3",
+ "react-router-dom": "6.4.2",
+ "react-toastify": "^9.1.1"
},
"devDependencies": {
"@types/react": "^18.0.17",
@@ -37,9 +41,10 @@
}
},
"lint-staged": {
- "*.{tsx,jsx,ts,js,css}": [
+ "*.{tsx,jsx,ts,js}": [
"eslint --cache --fix",
"prettier --write"
- ]
+ ],
+ "*.css": "prettier --write"
}
}
diff --git a/public/fonts/Inter.ttf b/public/fonts/Inter.ttf
new file mode 100644
index 0000000..ec3164e
Binary files /dev/null and b/public/fonts/Inter.ttf differ
diff --git a/public/fonts/Inter.woff b/public/fonts/Inter.woff
new file mode 100644
index 0000000..37ae329
Binary files /dev/null and b/public/fonts/Inter.woff differ
diff --git a/public/fonts/Inter.woff2 b/public/fonts/Inter.woff2
new file mode 100644
index 0000000..becbf35
Binary files /dev/null and b/public/fonts/Inter.woff2 differ
diff --git a/public/images/logo.jpg b/public/images/logo.jpg
new file mode 100644
index 0000000..0080afd
Binary files /dev/null and b/public/images/logo.jpg differ
diff --git a/src/components/CartDialog/CartDialog.module.css b/src/components/CartDialog/CartDialog.module.css
new file mode 100644
index 0000000..686e10a
--- /dev/null
+++ b/src/components/CartDialog/CartDialog.module.css
@@ -0,0 +1,75 @@
+.dialog {
+ z-index: 10;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ width: clamp(240px, 40vw, 440px);
+ max-height: 80vh;
+ overflow-y: auto;
+
+ padding: 24px;
+ background-color: white;
+ border-radius: 12px;
+ box-shadow: 4px 0px 16px var(--box-shadows);
+}
+
+.dialog__header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.dialog__title {
+ color: var(--accented-green);
+ font-size: var(--lg);
+ text-transform: uppercase;
+}
+
+.dialog__body {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.dialog__subtotal,
+.dialog__total {
+ display: flex;
+ justify-content: space-between;
+ margin-block: 12px;
+}
+
+.dialog__subtotal {
+ font-size: var(--sm);
+ color: var(--texts-light);
+}
+
+.dialog__total {
+ font-size: var(--md);
+ font-weight: bolder;
+}
+
+.dialog__total .rowText {
+ color: var(--accented-green);
+}
+
+.dialog__button {
+ text-decoration: none;
+ text-align: center;
+ display: block;
+ width: 100%;
+ margin-block: 16px 12px;
+ font-size: var(--sm);
+ background-color: var(--accented-green);
+ padding: 0.8em;
+ outline: none;
+ color: white;
+ border: 1px solid transparent;
+ border-radius: 12px;
+}
+
+.dialog__close,
+.dialog__button {
+ cursor: pointer;
+}
diff --git a/src/components/CartDialog/CartDialog.tsx b/src/components/CartDialog/CartDialog.tsx
new file mode 100644
index 0000000..9702a17
--- /dev/null
+++ b/src/components/CartDialog/CartDialog.tsx
@@ -0,0 +1,108 @@
+import Styles from './CartDialog.module.css';
+import { useContext } from 'react';
+import { Link, useNavigate } from 'react-router-dom';
+import { SessionContext } from '../../context/SessionContext';
+import { FiX } from 'react-icons/fi';
+import { CartDialogRow } from './CartDialogRow/CartDialogRow';
+import { toast } from 'react-toastify';
+
+interface IProps {
+ closeCallback: () => void;
+}
+
+export function CartDialog(props: IProps) {
+ const { isLoggedIn, cart, makeOrder } = useContext(SessionContext);
+ const navigate = useNavigate();
+
+ const GetCartTotal = () => {
+ const price = cart.reduce((acc, curr) => {
+ return acc + curr.price * curr.quantity;
+ }, 0);
+
+ return price.toFixed(2);
+ };
+
+ return (
+
+
+
Mi carrito
+ props.closeCallback()}
+ />
+
+
+ {cart.map((item, index) => {
+ return ;
+ })}
+
+ {/*
+ */}
+
+
Total
+
{GetCartTotal()}
+
+
+ Ir al carrito
+
+ {
+ // Redirects to login if the user is not authenticated
+ if (!isLoggedIn) {
+ toast.warn('Log in to create orders.', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+
+ navigate('/login');
+ return;
+ }
+
+ // Function to create an order
+ const process = async () => {
+ const done = await makeOrder();
+ if (done) {
+ toast.success('Order was created successfully', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ } else {
+ toast.error('Unable to create a new order. Try again.', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ }
+ };
+
+ // Use the function if there are somethint in the cart
+ if (cart.length > 0) {
+ process();
+ } else {
+ toast.warn(
+ 'Your cart is empty, please, add some product before trying to create a new order',
+ {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ },
+ );
+ }
+ }}
+ >
+ Realizar pedido
+
+
+ );
+}
diff --git a/src/components/CartDialog/CartDialogRow/CartDialogRow.module.css b/src/components/CartDialog/CartDialogRow/CartDialogRow.module.css
new file mode 100644
index 0000000..4a1ca62
--- /dev/null
+++ b/src/components/CartDialog/CartDialogRow/CartDialogRow.module.css
@@ -0,0 +1,51 @@
+.cartItem {
+ display: flex;
+ border-bottom: 1px solid var(--box-shadows);
+}
+
+.cartItem__image {
+ width: 20%;
+ min-width: 120px;
+ object-fit: contain;
+}
+
+.cartItem__title {
+ font-size: var(--md);
+ font-weight: 300;
+ color: var(--texts);
+}
+
+.cartItem__content {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 8px;
+ padding: 0px 12px;
+}
+
+.cartItem__footer {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.cartItem__price {
+ font-size: var(--md);
+}
+
+.cartItem__amount,
+.cartItem__inputGroup label {
+ font-weight: bolder;
+ display: inline;
+ margin-inline-end: 8px;
+ font-size: var(--sm);
+ color: var(--texts-light);
+}
+
+.cartItem__inputGroup input {
+ width: 50%;
+ border: 1px solid var(--box-shadows);
+ padding: 8px;
+ border-radius: 4px;
+ outline: none;
+}
diff --git a/src/components/CartDialog/CartDialogRow/CartDialogRow.tsx b/src/components/CartDialog/CartDialogRow/CartDialogRow.tsx
new file mode 100644
index 0000000..908d222
--- /dev/null
+++ b/src/components/CartDialog/CartDialogRow/CartDialogRow.tsx
@@ -0,0 +1,47 @@
+import { useContext } from 'react';
+import { SessionContext } from '../../../context/SessionContext';
+import Styles from './CartDialogRow.module.css';
+
+interface IProduct {
+ id: string;
+ image: string;
+ name: string;
+ units: string;
+ quantity: number;
+ price: number;
+}
+
+interface Iprops {
+ product: IProduct;
+}
+
+export function CartDialogRow(props: Iprops) {
+ const { updateCart } = useContext(SessionContext);
+
+ return (
+
+
+
+
{props.product.name}
+
{props.product.units}
+
+
+ Cantidad
+ {
+ // Update value on session
+ const value = e.target.value;
+ updateCart({ id: props.product.id, amount: value });
+ }}
+ >
+
+
{props.product.price}
+
+
+
+ );
+}
diff --git a/src/components/dynamicForm/DynamicForm.module.css b/src/components/dynamicForm/DynamicForm.module.css
new file mode 100644
index 0000000..b4b1f66
--- /dev/null
+++ b/src/components/dynamicForm/DynamicForm.module.css
@@ -0,0 +1,62 @@
+.form {
+ width: clamp(240px, 80vw, 480px);
+ padding: 8px 16px;
+ margin: 16px auto;
+ border: 1px solid var(--box-shadows);
+ box-shadow: 4px 0px 16px var(--box-shadows);
+}
+
+.form__title {
+ text-transform: uppercase;
+ font-weight: bolder;
+ text-align: center;
+ margin-block: 16px;
+ font-size: var(--lg);
+}
+
+.form__group {
+ display: block;
+ margin-block: 8px 16px;
+}
+
+.form__label {
+ display: block;
+ margin-block-end: 12px;
+ font-size: var(--sm);
+}
+
+.form__input,
+.form__submit {
+ outline: none;
+ border: 1px solid transparent;
+}
+
+.form__input {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid var(--box-shadows);
+ background-color: var(--inputs-bg);
+}
+
+.form__error {
+ display: none;
+ margin-block: 8px;
+ list-style: none;
+ font-size: var(--sm);
+ color: var(--accented-red);
+}
+
+.form__error__active {
+ display: block;
+}
+
+.form__submit {
+ cursor: pointer;
+ display: block;
+ margin-block: 12px;
+ width: 100%;
+ padding: 0.6em;
+ color: white;
+ background-color: var(--accented-green);
+ font-size: var(--sm);
+}
diff --git a/src/components/dynamicForm/DynamicForm.tsx b/src/components/dynamicForm/DynamicForm.tsx
new file mode 100644
index 0000000..32b3954
--- /dev/null
+++ b/src/components/dynamicForm/DynamicForm.tsx
@@ -0,0 +1,156 @@
+import { useState, ChangeEvent } from 'react';
+import Styles from './DynamicForm.module.css';
+import { toast } from 'react-toastify';
+
+// Props interfaces
+interface Field {
+ label: string;
+ name: string;
+ type: string;
+ placeholder: string;
+ minlength?: number;
+}
+
+interface Rule {
+ name: string;
+ regexp: RegExp;
+ message: string;
+ done: boolean;
+}
+
+interface Props {
+ title: string;
+ fields: Array;
+ submitLabel: string;
+ // Callback to handle input submit
+ // eslint-disable-next-line
+ callback: (payload: any) => Promise;
+ // Optional field to validate with regular expressions
+ rules?: Array;
+}
+
+type TValues = {
+ [key: string]: string;
+};
+
+export function DynamicForm(props: Props) {
+ // Store form current values
+ const init: TValues = {};
+ const [values, setValues] = useState(init);
+
+ // Update form values state when some input change
+ const handleInputChange = (event: ChangeEvent) => {
+ // Update value on state
+ const key = event.target.name;
+ const value = event.target.value;
+ setValues({ ...values, [key]: value });
+
+ // Return if no validation rules were given
+ if (!props.rules) return;
+
+ // Get the field rule
+ const rule: Rule = props.rules.filter((rule) => rule.name === key)[0];
+ // Return if the field has no rule to validate it
+ if (!rule) return;
+
+ const ruleIndex: number = props.rules.indexOf(rule);
+ const correct = rule.regexp.test(value);
+
+ if (!correct) {
+ const err: HTMLElement | null = document.getElementById(`error-${key}`);
+ if (err != null) {
+ err.textContent = rule.message;
+ err.classList.add(`${Styles.form__error__active}`);
+
+ // Update the done field on the rule to false
+ props.rules[ruleIndex].done = false;
+ }
+ } else {
+ const err: HTMLElement | null = document.getElementById(`error-${key}`);
+ if (err != null) err.classList.remove(`${Styles.form__error__active}`);
+
+ // Update the done field on the rule to false
+ props.rules[ruleIndex].done = true;
+ }
+ };
+
+ // Verify all the fields with the regular expressions
+ // and use the callback
+ const handleSubmit = () => {
+ let allFieldsOk = true;
+ const isSignup = props.fields.some((field) => field.name === 'password2');
+
+ if (isSignup) {
+ if (values['password'] != values['password2']) {
+ toast.error('Passords are not equals', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+
+ return;
+ }
+ }
+
+ if (props.rules) {
+ props.rules.forEach((rule) => {
+ if (!rule.done) {
+ allFieldsOk = false;
+ return; // Breaks the foreach
+ }
+ });
+
+ if (!allFieldsOk) {
+ // Show an information alert
+ toast.warn('Please, fill all the fields correctly before sending again.', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ } else {
+ props.callback(values);
+ }
+ } else {
+ props.callback(values);
+ }
+ };
+
+ // Generate fields
+ const generateFields = props.fields.map((field, index) => {
+ return (
+
+
+ {field.label}
+
+
+
+
+ );
+ });
+
+ return (
+
+ );
+}
diff --git a/src/components/modalproducts/ModalProducts.tsx b/src/components/modalproducts/ModalProducts.tsx
new file mode 100644
index 0000000..7c15fb9
--- /dev/null
+++ b/src/components/modalproducts/ModalProducts.tsx
@@ -0,0 +1,52 @@
+import Styles from './modalproducts.module.css';
+import { Iproduct } from '../../interfaces/interfaces';
+import { FiShoppingCart, FiHeart, FiXCircle } from 'react-icons/fi';
+//import { useNavigate } from 'react-router-dom';
+
+interface props {
+ product: Iproduct;
+ // eslint-disable-next-line no-unused-vars
+ CerrarCallBack: (product: Iproduct) => void;
+}
+
+export function ModalProduct(props: props) {
+ return (
+
+
+
props.CerrarCallBack(props.product)}
+ className={Styles.modal_btn_cerrar}
+ color={'#21a746'}
+ size={'1.4em'}
+ />
+
+
+
+
+
+
{props.product.name}
+
{props.product.units}
+
{props.product.price} €
+
{props.product.description}
+
+
+ Añadir a la canasta
+
+
+
+ {/* {
+ navigate('/carrito');
+ }}
+ className=''
+ >
+ Ir al carito
+
+
+ Realizar pedido
+ */}
+
+
+
+ );
+}
diff --git a/src/components/modalproducts/modalproducts.module.css b/src/components/modalproducts/modalproducts.module.css
new file mode 100644
index 0000000..745331d
--- /dev/null
+++ b/src/components/modalproducts/modalproducts.module.css
@@ -0,0 +1,86 @@
+.modal_product {
+ display: grid;
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ width: clamp(240px, 60vw, 500px);
+ transform: translate(-50%, -50%);
+ background-color: white;
+ box-shadow: 0px 4px 20px var(--box-shadows);
+ padding: 32px;
+ border-radius: 12px;
+ max-height: 80vh;
+ overflow-y: auto;
+ z-index: 10;
+}
+.corazon {
+ cursor: pointer;
+ position: absolute;
+ top: 16px;
+ right: 54px;
+}
+
+.modal_btn_cerrar {
+ cursor: pointer;
+ position: absolute;
+ top: 16px;
+ right: 16px;
+ border: none;
+}
+
+@media screen and (max-width: 768px) {
+ .imagen {
+ width: 40%;
+ margin: 0px auto;
+ }
+}
+
+.imagen {
+ width: 100%;
+ max-height: 250px;
+ object-fit: contain;
+}
+
+.titulo {
+ font-size: var(--lg);
+ color: #000000;
+ margin-block: 8px;
+}
+
+.unidades {
+ font-size: var(--sm);
+ font-weight: bolder;
+ color: var(--texts-light);
+ margin-block: 24px;
+ margin-block: 8px;
+}
+
+.price {
+ font-size: var(--lg);
+ font-weight: bolder;
+ color: var(--accented-green);
+ margin-block: 8px;
+}
+
+.description {
+ color: var(--texts);
+ line-height: 1.4;
+ margin-block: 32px;
+ font-weight: 200px;
+}
+
+.boton {
+ display: flex;
+ gap: 12px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ border-radius: 10px;
+ padding: 0.7em 1em;
+ font-size: var(--sm);
+ font-weight: bold;
+ min-width: max-content;
+ text-align: center;
+ border: none;
+ background-color: var(--accented-green);
+ color: white;
+}
diff --git a/src/components/navbar/Navbar.module.css b/src/components/navbar/Navbar.module.css
new file mode 100644
index 0000000..0704d32
--- /dev/null
+++ b/src/components/navbar/Navbar.module.css
@@ -0,0 +1,144 @@
+.navbarBlock {
+ display: flex;
+ align-items: center;
+ padding: 16px 8px;
+ /* margin-block-end: 24px; */
+ width: 100%;
+ height: 72px;
+ background-color: white;
+ box-shadow: 4px 0px 16px var(--box-shadows);
+}
+
+.navbarContainer {
+ display: flex;
+ gap: 16px;
+ align-items: center;
+}
+
+.navbar__brand img {
+ max-width: 140px;
+}
+
+.navbar__right {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24px;
+ flex-grow: 1;
+}
+
+.navbar__search {
+ cursor: pointer;
+}
+
+/* Search bar */
+.navbar__inputContainer {
+ position: relative;
+ flex-grow: 1;
+}
+
+.navbar__inputContainer input {
+ padding: 12px 16px;
+ border: 1px solid var(--accented-green);
+ border-radius: 12px;
+ outline: none;
+ font-size: var(--sm);
+ width: 100%;
+}
+
+#searchIcon {
+ position: absolute;
+ /*Center vertically*/
+ top: 50%;
+ transform: translateY(-50%);
+ right: 16px;
+}
+
+/* Navigation options */
+.navigation {
+ display: flex;
+ gap: 32px;
+ list-style: none;
+}
+
+.navigation__item {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ align-items: center;
+ color: var(--accented-green);
+ text-decoration: none;
+}
+
+.navigation__float {
+ z-index: 2;
+ position: absolute;
+ right: 0;
+ top: 100%;
+
+ visibility: hidden;
+ opacity: 0;
+ display: flex;
+ flex-direction: column;
+
+ width: max-content;
+ min-width: 240px;
+
+ list-style: none;
+ background-color: white;
+ color: #2f2f2f;
+ box-shadow: 0px 4px 16px var(--box-shadows);
+
+ transition: opacity 0.3s ease-in-out;
+}
+
+.navigation__floatVisible {
+ visibility: visible;
+ opacity: 1;
+}
+
+.navigation__floatItem {
+ display: flex;
+ gap: 12px;
+
+ text-decoration: none;
+ font-weight: bold;
+ color: #2f2f2f;
+ padding: 16px;
+}
+
+.navigation__floatItem {
+ border-bottom: 1px solid var(--box-shadows);
+}
+
+/* Responsive */
+@media screen and (max-width: 768px) {
+ .navbarBlock {
+ height: max-content;
+ padding: 16px 24px;
+ }
+
+ .navbarContainer {
+ flex-direction: column;
+ }
+
+ .navbar__right {
+ width: 100%;
+ }
+}
+
+@media screen and (max-width: 576px) {
+ .navbar__right {
+ flex-direction: column;
+ }
+
+ .navbar__inputContainer {
+ width: 100%;
+ }
+
+ .navigation {
+ width: 100%;
+ justify-content: space-between;
+ }
+}
diff --git a/src/components/navbar/Navbar.tsx b/src/components/navbar/Navbar.tsx
new file mode 100644
index 0000000..6ae64df
--- /dev/null
+++ b/src/components/navbar/Navbar.tsx
@@ -0,0 +1,177 @@
+import Styles from './Navbar.module.css';
+import { useContext, ChangeEvent, useRef, useState } from 'react';
+import { NavLink, Link, useNavigate } from 'react-router-dom';
+import { SessionContext } from '../../context/SessionContext';
+import {
+ FiHeart,
+ FiUser,
+ FiShoppingCart,
+ FiSearch,
+ FiLock,
+ FiUserCheck,
+ FiUserX,
+} from 'react-icons/fi';
+import { FilterContext } from '../../context/FilterContext';
+import { CartDialog } from '../CartDialog/CartDialog';
+
+export function Navbar() {
+ // Fucntion from the provider
+ const { setCriteria, filterProducts } = useContext(FilterContext);
+ const [openCartDialog, setOpenCartDialog] = useState(false);
+ const { isLoggedIn, logout } = useContext(SessionContext);
+
+ const floatingOptions = useRef(null);
+ const navigate = useNavigate();
+
+ // Update provider's criteria
+ const handleInputChange = (e: ChangeEvent) => {
+ const value = e.target.value;
+ setCriteria(value);
+ };
+
+ // Const handle dropdown click
+ const handleDropDownClick = () => {
+ if (!floatingOptions) return;
+ if (!floatingOptions.current) return;
+
+ floatingOptions.current.classList.toggle(`${Styles.navigation__floatVisible}`);
+ };
+
+ // Float options for logged in user
+ const LoggedInOptions = () => {
+ return (
+ <>
+
+
+
+ Mi Cuenta
+
+
+
+
+
+ Mis favoritos
+
+
+
+
+
+ Mi Carrito
+
+
+
+ {
+ logout();
+ }}
+ >
+
+ Cerrar sesión
+
+
+ >
+ );
+ };
+
+ // Float options for not logged in user
+ const NotLoggedInOptions = () => {
+ return (
+ <>
+
+
+
+ Entrar
+
+
+
+
+
+ Crear cuenta
+
+
+ >
+ );
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+ {/* Searchbar and navigation container*/}
+
+
+ {
+ if (e.key == 'Enter') {
+ console.log('Filtering because of enter key pressed');
+ filterProducts();
+ navigate('/');
+ }
+ }}
+ >
+ {
+ filterProducts();
+ navigate('/');
+ }}
+ />
+
+
+
+
+
+ Favorites
+
+
+ {
+ handleDropDownClick();
+ }}
+ >
+
+
+
Account
+
+ {isLoggedIn ? LoggedInOptions() : NotLoggedInOptions()}
+
+
+
+ {
+ setOpenCartDialog(!openCartDialog);
+ }}
+ >
+
+
+ Cart
+
+
+
+
+
+
+ {openCartDialog ? (
+ {
+ setOpenCartDialog(false);
+ }}
+ />
+ ) : (
+ ''
+ )}
+ >
+ );
+}
diff --git a/src/components/productCard/ProductCard.module.css b/src/components/productCard/ProductCard.module.css
new file mode 100644
index 0000000..a2c914b
--- /dev/null
+++ b/src/components/productCard/ProductCard.module.css
@@ -0,0 +1,95 @@
+.product {
+ position: relative;
+ width: 320px;
+ border-radius: 4px;
+ padding: 4px 8px;
+ overflow: hidden;
+ border: 1px solid var(--box-shadows);
+
+ transition: all 0.3s ease;
+}
+
+.product__heart {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+}
+
+.product__image {
+ width: 100%;
+ object-fit: contain;
+}
+
+.product__name {
+ font-size: var(--lg);
+ font-weight: bold;
+ text-align: center;
+ margin-block-end: 24px;
+
+ /*Max text width*/
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.product__units,
+.product__price {
+ display: block;
+ text-align: center;
+ margin-block: 16px;
+}
+
+.product__units {
+ font-size: var(--sm);
+ color: var(--texts);
+}
+
+.product__price {
+ font-size: var(--md);
+ font-weight: bold;
+ color: var(--accented-green);
+}
+
+.product__button {
+ /*Center text and icon*/
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+ opacity: 0;
+
+ margin-block: 12px;
+ padding: 0.8em;
+ outline: none;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ font-size: var(--sm);
+
+ cursor: pointer;
+ background-color: var(--accented-yellow);
+ color: white;
+
+ transition: opacity 0.3s ease;
+}
+
+/*Hover effects*/
+.product:hover {
+ scale: 1.02;
+ box-shadow: 0px 4px 16px var(--box-shadows);
+}
+
+.product:hover .product__name {
+ white-space: normal;
+}
+
+.product:hover .product__button {
+ opacity: 1;
+}
+
+/* Responsive */
+@media screen and (max-width: 768px) {
+ .product {
+ width: 240px;
+ }
+}
diff --git a/src/components/productCard/ProductCard.tsx b/src/components/productCard/ProductCard.tsx
new file mode 100644
index 0000000..fa67622
--- /dev/null
+++ b/src/components/productCard/ProductCard.tsx
@@ -0,0 +1,119 @@
+import Styles from './ProductCard.module.css';
+import { Iproduct, ICartItem } from '../../interfaces/interfaces';
+import { FiShoppingCart, FiHeart } from 'react-icons/fi';
+import { FaHeart } from 'react-icons/fa';
+import { useContext } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { SessionContext } from '../../context/SessionContext';
+import { toast } from 'react-toastify';
+
+interface props {
+ product: Iproduct;
+ // eslint-disable-next-line no-unused-vars
+ CallBack: (product: Iproduct) => void;
+}
+
+// producto: producto
+// dialogcallback: funcion para abrir el modal
+export function ProductCard(props: props) {
+ const { isLoggedIn, addToCart, favorites, addToFavorites, removeFromFavorites } =
+ useContext(SessionContext);
+
+ const navigate = useNavigate();
+
+ const HandleAddToCart = async () => {
+ const CartItem: ICartItem = {
+ id: props.product.id,
+ name: props.product.name,
+ units: props.product.units,
+ quantity: 0,
+ price: props.product.price,
+ image: props.product.image,
+ };
+
+ const wasAdded = await addToCart(CartItem);
+
+ if (wasAdded) {
+ toast.success(`Successfully added ${props.product.name} to the cart`, {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ } else {
+ toast.error(`Unable to add ${props.product.name} to the cart. Try again.`, {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ }
+ };
+
+ return (
+
+ {favorites.some((id) => id === props.product.id) ? (
+ {
+ removeFromFavorites(props.product.id);
+ }}
+ />
+ ) : (
+ {
+ if (isLoggedIn) {
+ addToFavorites(props.product.id);
+ } else {
+ navigate('/login');
+
+ toast.warn('Log in to manage your favorites', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ }
+ }}
+ />
+ )}
+ {
+ props.CallBack(props.product);
+ }}
+ className={Styles.product__image}
+ src={props.product.image}
+ alt={props.product.name}
+ />
+ {props.product.name}
+ {props.product.units !== '' && (
+ {props.product.units}
+ )}
+ {props.product.price + '€'}
+ {
+ if (isLoggedIn) {
+ HandleAddToCart();
+ } else {
+ navigate('/login');
+
+ toast.warn('Log in to manage your cart', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ }
+ }}
+ >
+ Add to cart
+
+
+ );
+}
diff --git a/src/components/slider/Slider.module.css b/src/components/slider/Slider.module.css
new file mode 100644
index 0000000..f6d7a6a
--- /dev/null
+++ b/src/components/slider/Slider.module.css
@@ -0,0 +1,52 @@
+.filterContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin: auto;
+ padding: 10px 32px;
+ justify-content: center;
+}
+
+.slider {
+ width: 100%;
+}
+
+.rangeContainer {
+ width: 100%;
+}
+
+.inputPrices {
+ display: flex;
+ gap: 16px;
+ justify-content: space-between;
+}
+
+.inputPrices input {
+ margin-block: 12px;
+ padding: 0.8em;
+ width: 100%;
+}
+
+.inputPrices div {
+ display: flex;
+ width: 50%;
+ flex-direction: column;
+}
+
+.filterButton {
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+
+ margin-block: 12px;
+ padding: 0.8em;
+ outline: none;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ font-size: var(--sm);
+
+ cursor: pointer;
+ background-color: var(--accented-green);
+ color: white;
+}
diff --git a/src/components/slider/Slider.tsx b/src/components/slider/Slider.tsx
new file mode 100644
index 0000000..cda7632
--- /dev/null
+++ b/src/components/slider/Slider.tsx
@@ -0,0 +1,140 @@
+import Styles from './Slider.module.css';
+/* import { getProductsFiltrated } from '../../services/products.service';
+ */
+import { FilterContext } from '../../context/FilterContext';
+import { useContext, useRef } from 'react';
+
+export function Slider() {
+ // Value from the provider
+ const { min, max, filterProducts, setMax, setMin } = useContext(FilterContext);
+
+ const rangeInput = useRef(null);
+ const firstNumInput = useRef(null);
+ const secndNumInput = useRef(null);
+
+ let timeout = -1;
+ let timeout_2 = -1;
+
+ //adding event to range input
+ const onRangeListener = () => {
+ if (!firstNumInput.current || !secndNumInput.current || !rangeInput.current) return;
+
+ const minVal = rangeInput.current.value;
+
+ //if value from inputdesde is >= to inputhasta
+ if (parseFloat(rangeInput.current.value) >= parseFloat(secndNumInput.current.value)) {
+ clearTimeout(timeout);
+ timeout = setTimeout(function () {
+ if (!firstNumInput.current || !secndNumInput.current || !rangeInput.current) return;
+
+ firstNumInput.current.value = `${parseFloat(secndNumInput.current.value) - 1}`;
+ rangeInput.current.value = `${parseFloat(secndNumInput.current.value) - 1}`;
+ setMin(parseFloat(firstNumInput.current.value));
+ setMax(parseFloat(secndNumInput.current.value));
+ }, 820);
+ } else {
+ firstNumInput.current.value = minVal;
+ setMin(parseFloat(firstNumInput.current.value));
+ setMax(parseFloat(secndNumInput.current.value));
+ }
+ };
+
+ //adding event to inputs desde and hasta
+ const onInputListener = () => {
+ // string
+ if (!firstNumInput.current || !secndNumInput.current || !rangeInput.current) return;
+ const minVal = parseFloat(firstNumInput.current.value);
+
+ clearTimeout(timeout_2);
+ if (
+ parseFloat(firstNumInput.current.value) >= parseFloat(secndNumInput.current.value) &&
+ parseFloat(secndNumInput.current.value) != 0
+ ) {
+ timeout_2 = setTimeout(function () {
+ if (!firstNumInput.current || !secndNumInput.current || !rangeInput.current) return;
+ rangeInput.current.value = `${parseFloat(secndNumInput.current.value) - 1}`;
+ firstNumInput.current.value = `${parseFloat(secndNumInput.current.value) - 1}`;
+ setMin(parseFloat(firstNumInput.current.value));
+ setMax(parseFloat(secndNumInput.current.value));
+ }, 820);
+ } else {
+ rangeInput.current.value = `${minVal}`;
+ setMin(parseFloat(rangeInput.current.value));
+ setMax(parseFloat(secndNumInput.current.value));
+ }
+
+ if (parseFloat(secndNumInput.current.value) > 81.8) {
+ secndNumInput.current.value = `${81.8}`;
+ setMax(parseFloat(secndNumInput.current.value));
+ }
+
+ if (parseFloat(firstNumInput.current.value) < 0) {
+ firstNumInput.current.value = `${0}`;
+ setMin(parseFloat(firstNumInput.current.value));
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/src/config/config.ts b/src/config/config.ts
new file mode 100644
index 0000000..61442dc
--- /dev/null
+++ b/src/config/config.ts
@@ -0,0 +1,3 @@
+export const GLOBALS = {
+ API_HOST: import.meta.env.VITE_API_HOST || 'http://localhost:3030',
+};
diff --git a/src/context/FilterContext.tsx b/src/context/FilterContext.tsx
new file mode 100644
index 0000000..77841cb
--- /dev/null
+++ b/src/context/FilterContext.tsx
@@ -0,0 +1,109 @@
+import { useState, createContext, useEffect, ReactNode } from 'react';
+import { getProducts, getProductImage } from '../services/products.service';
+import { Iproduct } from '../interfaces/interfaces';
+import { getProductsFiltrated } from '../services/products.service';
+
+interface Props {
+ children: ReactNode;
+}
+
+//add function filter, return axios response
+interface IFilterContext {
+ criteria: string;
+ min: number;
+ max: number;
+ // eslint-disable-next-line no-unused-vars
+ setCriteria: (criteria: string) => void;
+ // eslint-disable-next-line no-unused-vars
+ setMin: (min: number) => void;
+ // eslint-disable-next-line no-unused-vars
+ setMax: (max: number) => void;
+ inventory: Array;
+ products: Array;
+ // eslint-disable-next-line no-unused-vars
+ setProducts: (products: Array) => void;
+ // eslint-disable-next-line no-unused-vars
+ filterProducts: () => void;
+}
+
+//default value return of function
+// Create context with default values
+export const FilterContext = createContext({
+ criteria: '',
+ min: 0,
+ max: 81.8,
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
+ setCriteria: (criteria: string) => {},
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
+ setMin: (min: number) => {},
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
+ setMax: (max: number) => {},
+ inventory: [],
+ products: [],
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
+ setProducts: (products: Array) => {},
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
+ filterProducts: () => {},
+});
+
+// Component
+export const FilterContextProvider = ({ children }: Props) => {
+ const [inventory, setInventory] = useState(new Array());
+ const [products, setProducts] = useState(new Array());
+
+ const [criteria, setCriteria] = useState('');
+ const [min, setMin] = useState(0);
+ const [max, setMax] = useState(81.8);
+
+ useEffect(() => {
+ const load = async () => {
+ const response = await getProducts();
+ const items: Array = response.products;
+ const products: Array = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const imageReply = await getProductImage(items[i].serial);
+ //console.log(imageReply);
+ products.push({ ...items[i], image: imageReply.image });
+ }
+
+ setInventory(products);
+ };
+
+ load();
+ }, []);
+
+ //updating products to show from filter
+ const filterProducts = async () => {
+ const response = await getProductsFiltrated(min, max, criteria);
+ const items: Array = response.products;
+ const products: Array = [];
+
+ if (items != undefined) {
+ for (let i = 0; i < items.length; i++) {
+ const imageReply = await getProductImage(items[i].serial);
+ products.push({ ...items[i], image: imageReply.image });
+ }
+ setInventory(products);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/context/SessionContext.tsx b/src/context/SessionContext.tsx
new file mode 100644
index 0000000..cf0f8b9
--- /dev/null
+++ b/src/context/SessionContext.tsx
@@ -0,0 +1,290 @@
+import { AxiosResponse } from 'axios';
+import { createContext, useState, useEffect, ReactNode } from 'react';
+import { IUser, ICartItem } from '../interfaces/interfaces';
+import { IUpdateCartPayload } from '../interfaces/interfaces.services';
+import { UserTemplate } from '../templates/user';
+
+import { OrderService, UpdateCartItemAmount } from '../services/shop.services';
+import { GetProductImageFromEndpointService } from '../services/products.service';
+import {
+ WhoamiService,
+ LogoutService,
+ GetCartService,
+ RemoveFromCartService,
+ AddToCartService,
+ GetFavoritesService,
+ RemoveFromFavoritesService,
+ AddToFavoritesService,
+} from '../services/session.services';
+
+interface Props {
+ children: ReactNode;
+}
+
+interface ISessionCTX {
+ user: IUser;
+ isLoggedIn: boolean;
+ isSessionLoading: boolean;
+ cart: Array;
+ favorites: Array;
+ // eslint-disable-next-line no-unused-vars
+ login: (response: AxiosResponse) => Promise;
+ logout: () => Promise;
+ // eslint-disable-next-line no-unused-vars
+ removeFromCart: (id: string) => Promise;
+ // eslint-disable-next-line no-unused-vars
+ addToCart: (item: ICartItem) => Promise;
+ // eslint-disable-next-line no-unused-vars
+ updateCart: (payload: IUpdateCartPayload) => Promise;
+ // eslint-disable-next-line no-unused-vars
+ removeFromFavorites: (id: string) => Promise;
+ // eslint-disable-next-line no-unused-vars
+ addToFavorites: (id: string) => Promise;
+ makeOrder: () => Promise;
+}
+
+// Here we define the default values for each
+// field of the ISessionCTX interface
+export const SessionContext = createContext({
+ user: UserTemplate,
+ isLoggedIn: false,
+ isSessionLoading: false,
+ cart: [],
+ favorites: [],
+ // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-empty-function
+ login: async function (payload: AxiosResponse) {},
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
+ logout: async function () {},
+ // eslint-disable-next-line no-unused-vars
+ removeFromCart: async function (id: string) {
+ return true;
+ },
+ // eslint-disable-next-line no-unused-vars
+ addToCart: async function (item: ICartItem) {
+ return true;
+ },
+ // eslint-disable-next-line no-unused-vars
+ updateCart: async function (payload: IUpdateCartPayload) {
+ return true;
+ },
+ // eslint-disable-next-line no-unused-vars,
+ removeFromFavorites: async function (id: string) {
+ return true;
+ },
+ // eslint-disable-next-line no-unused-vars,
+ addToFavorites: async function (id: string) {
+ return true;
+ },
+ makeOrder: async function () {
+ return true;
+ },
+});
+
+// Session provider
+export const SessionContextProvider = ({ children }: Props) => {
+ const [user, setUser] = useState(UserTemplate);
+ const [cart, setCart] = useState(Array);
+ const [favorites, setFavorites] = useState(Array);
+ const [isSessionLoading, setIsSessionLoading] = useState(false);
+ const [isLoggedIn, setIsLoggedIn] = useState(false);
+
+ // Try to get the user information on reload
+ // or new start
+ useEffect(() => {
+ const recover = async () => {
+ setIsSessionLoading(true);
+
+ const reply = await WhoamiService(1); // First iteration
+
+ if (!reply) {
+ setIsSessionLoading(false);
+ return;
+ }
+
+ if (reply.status === 200) {
+ setUser(reply.data.user);
+ setIsLoggedIn(true);
+ }
+
+ setIsSessionLoading(false);
+ };
+
+ recover();
+ }, []);
+
+ // Get user cart and favorites on load
+ useEffect(() => {
+ const getUserCart = async () => {
+ const reply = await GetCartService(1); // First try
+ if (!reply?.data) return;
+
+ const items = reply.data.products;
+ const products: Array = [];
+
+ for (let i = 0; i < items.length; i++) {
+ const response = await GetProductImageFromEndpointService(items[i].image);
+ products.push({ ...items[i], image: response.image });
+ }
+
+ setCart(products);
+ };
+
+ const getUserFavorites = async () => {
+ const reply = await GetFavoritesService(1); // First try
+ if (!reply?.data) return;
+ setFavorites(reply.data.favorites);
+ };
+
+ if (isLoggedIn) {
+ // Only get when the user have an active session
+ getUserCart();
+ getUserFavorites();
+ }
+ }, [isLoggedIn]);
+
+ // Actual value for login function
+ const login = async (response: AxiosResponse) => {
+ setIsSessionLoading(true);
+ setUser(response.data.user);
+ setIsLoggedIn(true);
+ setIsSessionLoading(false);
+ };
+
+ // Remove item from cart
+ const removeFromCart = async (id: string) => {
+ const wasDeleted = await RemoveFromCartService(1, id);
+
+ if (wasDeleted) {
+ const newCart = cart.filter((product) => product.id != id);
+ setCart(newCart);
+ return true;
+ }
+
+ return false;
+ };
+
+ // Update amount for some item on cart
+ const updateCart = async (payload: IUpdateCartPayload) => {
+ if (payload.amount === '') return false;
+
+ const newCart = cart.map((product) => {
+ if (product.id === payload.id) {
+ return { ...product, quantity: parseInt(payload.amount) };
+ } else {
+ return product;
+ }
+ });
+
+ // Update on database
+ const wasUpdated = await UpdateCartItemAmount(1, payload);
+
+ if (wasUpdated) {
+ setCart(newCart);
+ return true;
+ }
+
+ return false;
+ };
+
+ // Logout
+ const logout = async () => {
+ setIsSessionLoading(true);
+ const wasSessionClosed = await LogoutService(1);
+
+ if (wasSessionClosed) {
+ setIsLoggedIn(false);
+ setUser(UserTemplate);
+ setCart([]);
+ setFavorites([]);
+ }
+
+ setIsSessionLoading(false);
+ };
+
+ // Add item to cart
+ const addToCart = async (item: ICartItem) => {
+ const wasAdded = await AddToCartService(1, item.id);
+
+ if (wasAdded) {
+ const exists = cart.some((product) => product.id === item.id);
+
+ if (exists) {
+ const newCart = cart.map((product) => {
+ if (product.id === item.id) {
+ return { ...product, quantity: product.quantity + 1 };
+ } else {
+ return product;
+ }
+ });
+
+ setCart(newCart);
+ } else {
+ setCart([...cart, { ...item, quantity: 1 }]);
+ }
+
+ return true;
+ }
+
+ return false;
+ };
+
+ // Remove item from favorites
+ const removeFromFavorites = async (id: string) => {
+ const wasDeleted = await RemoveFromFavoritesService(1, id);
+ console.log(wasDeleted);
+
+ if (wasDeleted) {
+ const newFavorites = favorites.filter((fid) => fid !== id);
+ setFavorites(newFavorites);
+ return true;
+ }
+
+ return false;
+ };
+
+ // Add item to favorites
+ const addToFavorites = async (id: string) => {
+ const wasAdded = await AddToFavoritesService(1, id);
+
+ if (wasAdded) {
+ setFavorites([...favorites, id]);
+ return true;
+ }
+
+ return false;
+ };
+
+ // Create order from cart
+ const makeOrder = async () => {
+ const wasCreated = await OrderService(1);
+
+ if (wasCreated) {
+ setCart([]); // Empty cart
+ return true;
+ }
+
+ return false;
+ };
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/src/global.css b/src/global.css
new file mode 100644
index 0000000..ce7d322
--- /dev/null
+++ b/src/global.css
@@ -0,0 +1,48 @@
+/*Configure global fonts*/
+@font-face {
+ font-family: Inter;
+ src: url('fonts/Inter.woff2') format('woff2'), url('fonts/Inter.woff') format('woff'),
+ url('fonts/Inter.ttf') format('truetype');
+ font-display: swap;
+}
+
+/* Global variables */
+:root {
+ /*Colors*/
+ --opaque-yellow: #f3b24a;
+ --accented-yellow: #efa229;
+
+ --opaque-red: #ef2947;
+ --accented-red: #e80729;
+
+ --opaque-blue: #74a4ec;
+ --accented-blue: #507bbb;
+
+ --opaque-green: #58d17a;
+ --accented-green: #21a746;
+
+ --dark-green: #006414;
+ --inputs-bg: #f7f6fb;
+ --box-shadows: #e1e1e1;
+ --texts-light: #a5a5a5;
+ --texts: #4e4e4e;
+
+ /*Font sizes*/
+ --sm: 1rem;
+ --md: 1.2rem;
+ --lg: 1.4rem;
+}
+
+/* Global styles */
+* {
+ font-family: Inter, sans-serif;
+ box-sizing: border-box;
+ margin: 0px;
+ padding: 0px;
+ accent-color: var(--accented-green);
+}
+
+.container {
+ width: 1400px;
+ margin-inline: auto;
+}
diff --git a/src/interfaces/interfaces.services.ts b/src/interfaces/interfaces.services.ts
new file mode 100644
index 0000000..fc36e29
--- /dev/null
+++ b/src/interfaces/interfaces.services.ts
@@ -0,0 +1,16 @@
+export interface ILoginPayload {
+ email: string;
+ password: string;
+}
+
+export interface ISignupPayload {
+ firstname: string;
+ lastname: string;
+ email: string;
+ password: string;
+}
+
+export interface IUpdateCartPayload {
+ id: string;
+ amount: string;
+}
diff --git a/src/interfaces/interfaces.ts b/src/interfaces/interfaces.ts
new file mode 100644
index 0000000..9e619f8
--- /dev/null
+++ b/src/interfaces/interfaces.ts
@@ -0,0 +1,43 @@
+export interface Iproduct {
+ id: string;
+ serial: number;
+ name: string;
+ image: string;
+ units: string;
+ annotations: string;
+ discount: number;
+ price: number;
+ description: string;
+}
+
+export interface ICartItem {
+ id: string;
+ name: string;
+ units: string;
+ quantity: number;
+ price: number;
+ image: string;
+}
+
+export interface ModalProduct {
+ id: string;
+ name: string;
+ image: string;
+ units: string;
+ annotations: string;
+ discount: number;
+ price: number;
+ description: string;
+}
+
+export interface IUser {
+ id: number;
+ email: string;
+ firstname: string;
+ lastname: string;
+}
+
+// Session provider interface
+export interface ISessionProvider {
+ user: IUser;
+}
diff --git a/src/main.tsx b/src/main.tsx
index 41bf61e..a4b2713 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,15 +2,42 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
+import { ToastContainer } from 'react-toastify';
+import 'react-toastify/dist/ReactToastify.css';
+
+// Global styles
+import './global.css';
+
+// Contexts
+import { FilterContextProvider } from './context/FilterContext';
+import { SessionContextProvider } from './context/SessionContext';
+
// Components
-import { Starter } from './pages/Starter';
+import { Navbar } from './components/navbar/Navbar';
+import { ProductsGrid } from './pages/productsGrid/ProductsGrid';
+import { Favorites } from './pages/Favorites/Favorites';
+import { Login } from './pages/login/Login';
+import { Signup } from './pages/signup/Signup';
+import { ShopCart } from './pages/shopcart/ShopCart';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
-
- }>
-
+
+
+
+
+
+ }>
+
+
+
+ }>
+ }>
+ }>
+ }>
+
+
,
);
diff --git a/src/pages/Favorites/Favorites.module.css b/src/pages/Favorites/Favorites.module.css
new file mode 100644
index 0000000..a4d7eab
--- /dev/null
+++ b/src/pages/Favorites/Favorites.module.css
@@ -0,0 +1,11 @@
+/*Cards container*/
+.products {
+ padding-block-start: 16px;
+ padding-block-end: 70px;
+ height: max-content;
+ display: flex;
+ justify-content: center;
+ align-items: stretch;
+ flex-wrap: wrap;
+ gap: 24px;
+}
diff --git a/src/pages/Favorites/Favorites.tsx b/src/pages/Favorites/Favorites.tsx
new file mode 100644
index 0000000..b935613
--- /dev/null
+++ b/src/pages/Favorites/Favorites.tsx
@@ -0,0 +1,91 @@
+import Styles from './Favorites.module.css';
+import { useEffect, useState, useContext } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { SessionContext } from '../../context/SessionContext';
+import { GetDetailedFavoritesService } from '../../services/user.services';
+import { GetProductImageFromEndpointService } from '../../services/products.service';
+import { Iproduct } from '../../interfaces/interfaces';
+import { ProductCard } from '../../components/productCard/ProductCard';
+import { ModalProduct } from '../../components/modalproducts/ModalProducts';
+
+import { toast } from 'react-toastify';
+
+export function Favorites() {
+ const { isSessionLoading, isLoggedIn } = useContext(SessionContext);
+ const [favorites, setFavorites] = useState(Array);
+ const [viewModal, setViewModal] = useState(false);
+ const navigate = useNavigate();
+
+ // Redirect to home if is not logged in
+ useEffect(() => {
+ console.log(isSessionLoading, isLoggedIn);
+ if (!isSessionLoading && !isLoggedIn) {
+ // Show an information alert
+ toast.warn('Please, log in to see your favorites', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+
+ // Redirect to heme because there is an active session
+ navigate('/login');
+ }
+ }, [isSessionLoading, isLoggedIn]);
+
+ // Datos del producto actual que es mostrado en el modal
+ const [modalData, setModalData] = useState({
+ id: '',
+ serial: 0,
+ name: '',
+ image: '',
+ units: '',
+ annotations: '',
+ discount: 0,
+ price: 0,
+ description: '',
+ });
+
+ useEffect(() => {
+ const getCurrentFavorites = async () => {
+ const response = await GetDetailedFavoritesService(1);
+ const items = response.data.favorites;
+ const products: Array = [];
+
+ if (response.status === 200) {
+ for (let i = 0; i < items.length; i++) {
+ const ireply = await GetProductImageFromEndpointService(items[i].image);
+ products.push({ ...items[i], image: ireply.image });
+ }
+
+ setFavorites(products);
+ }
+ };
+
+ getCurrentFavorites();
+ }, []);
+
+ // Funcion para abrir el modal
+ const handleAbrir = (data: Iproduct) => {
+ // Se cambian los datos del modal con los datos del producto
+ setModalData({ ...data });
+ // Se cambia la vista del modal a true
+ setViewModal(true);
+ };
+
+ // Funcion para cerrar el modal
+ const handleCerrar = () => {
+ setViewModal(false);
+ };
+
+ return (
+ <>
+
+ {favorites.map((product, index) => {
+ return ;
+ })}
+
+ {viewModal ? : ''}
+ >
+ );
+}
diff --git a/src/pages/Starter.tsx b/src/pages/Starter.tsx
deleted file mode 100644
index 3b5f962..0000000
--- a/src/pages/Starter.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export function Starter() {
- return Hello World ;
-}
diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx
new file mode 100644
index 0000000..1a1106f
--- /dev/null
+++ b/src/pages/login/Login.tsx
@@ -0,0 +1,96 @@
+import { useContext, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { SessionContext } from '../../context/SessionContext';
+import { DynamicForm } from '../../components/dynamicForm/DynamicForm';
+import { ILoginPayload } from '../../interfaces/interfaces.services';
+import { LoginService } from '../../services/session.services';
+
+import { toast } from 'react-toastify';
+
+export function Login() {
+ // Get login function from provider
+ const { login, isLoggedIn, isSessionLoading } = useContext(SessionContext);
+ const navigate = useNavigate();
+
+ // Redirect to home if is logged in
+ useEffect(() => {
+ if (!isSessionLoading && isLoggedIn) {
+ // Show an information alert
+ toast.warn('You already have an active session', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+
+ // Redirect to heme because there is an active session
+ navigate('/');
+ }
+ }, [isSessionLoading, isLoggedIn]);
+
+ // Prepare callback
+ const HandleLoginSubmit = async (payload: ILoginPayload) => {
+ // Get response from backk-end
+ const response = await LoginService(payload);
+ const data = response?.data;
+
+ if (response?.status !== 200) {
+ // Shows an error alert
+ toast.error(data.message, {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ } else {
+ // Update the session on the provider component
+ login(response);
+
+ toast.success('Login successfully completed', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+
+ navigate('/');
+ }
+ };
+
+ const loginFields = [
+ {
+ label: 'Email',
+ name: 'email',
+ type: 'email',
+ placeholder: 'foo@bar.com',
+ },
+ {
+ label: 'Password',
+ name: 'password',
+ type: 'password',
+ placeholder: '********',
+ minlength: 8,
+ },
+ ];
+
+ // Regular expressions to validate fields
+ const loginRules = [
+ {
+ name: 'password',
+ done: false,
+ regexp: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$!%*#?&/%])[A-Za-z\d$!%*#?&/%]{8,}$/,
+ message:
+ 'Password must have minimal length of 8 characters, contains at least one number, one letter and one special character',
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/src/pages/productsGrid/ProductsGrid.module.css b/src/pages/productsGrid/ProductsGrid.module.css
new file mode 100644
index 0000000..9f62da9
--- /dev/null
+++ b/src/pages/productsGrid/ProductsGrid.module.css
@@ -0,0 +1,59 @@
+.gridLayout {
+ display: grid;
+ grid-template-columns: 400px 1fr;
+ min-height: calc(100vh - 72px);
+}
+
+/* Todo: Filters styles by @SilviaPabo/n */
+.productsFilters {
+ box-shadow: 0px 4px 16px var(--box-shadows);
+ min-height: calc(100vh - 72px);
+}
+
+/*Cards container*/
+.products {
+ padding-block-start: 16px;
+ padding-block-end: 70px;
+ height: max-content;
+ display: flex;
+ justify-content: center;
+ align-items: stretch;
+ flex-wrap: wrap;
+ gap: 24px;
+}
+
+/* Responsive */
+@media screen and (max-width: 768px) {
+ .gridLayout {
+ grid-template-columns: 1fr;
+ }
+
+ .productsFilters {
+ height: 200px;
+ }
+}
+
+.pagination {
+ position: fixed;
+ right: 32px;
+ bottom: 0px;
+ margin-block: 16px;
+}
+
+.pagination__item {
+ cursor: pointer;
+ margin-inline: 4px;
+ padding: 8px 12px;
+ font-size: var(--md);
+ font-weight: bold;
+ background-color: white;
+ color: var(--texts);
+ border: 1px solid #bebebe;
+ box-shadow: 0px 0px 8px var(--box-shadows);
+ list-style-type: none;
+ display: inline-block;
+}
+
+.pagination__active {
+ color: var(--accented-green);
+}
diff --git a/src/pages/productsGrid/ProductsGrid.tsx b/src/pages/productsGrid/ProductsGrid.tsx
new file mode 100644
index 0000000..258b241
--- /dev/null
+++ b/src/pages/productsGrid/ProductsGrid.tsx
@@ -0,0 +1,84 @@
+import Styles from './ProductsGrid.module.css';
+import { useState, useContext, useEffect } from 'react';
+import { Iproduct } from '../../interfaces/interfaces';
+import { ProductCard } from '../../components/productCard/ProductCard';
+import { ModalProduct } from '../../components/modalproducts/ModalProducts';
+import ReactPaginate from 'react-paginate';
+import { Slider } from '../../components/slider/Slider';
+
+import { FilterContext } from '../../context/FilterContext';
+
+export function ProductsGrid() {
+ const { products, inventory, setProducts } = useContext(FilterContext);
+
+ // Estado que guarda los datos del producto
+ const [modalData, setModalData] = useState({
+ id: '',
+ serial: 0,
+ name: '',
+ image: '',
+ units: '',
+ annotations: '',
+ discount: 0,
+ price: 0,
+ description: '',
+ });
+
+ // Estado que indica si el modal esta abierto o no
+ const [viewModal, setViewModal] = useState(false);
+
+ // Funcion para abrir el modal
+ const handleAbrir = (data: Iproduct) => {
+ // Se cambian los datos del modal con los datos del producto
+ setModalData({ ...data });
+ // Se cambia la vista del modal a true
+ setViewModal(true);
+ };
+
+ // Funcion para cerrar el modal
+ const handleCerrar = () => {
+ setViewModal(false);
+ };
+
+ //PAGINATION
+
+ const handlePageClick = async (data: { selected: number }) => {
+ const start: number = data.selected * 12;
+ const end: number = start + 12;
+ const page: Array = inventory.slice(start, end);
+ setProducts(page);
+ };
+
+ //setinventario y setcontext setproducts vienen del contexto
+ useEffect(() => {
+ setProducts(inventory.slice(0, 12));
+ }, [inventory]);
+
+ return (
+
+
+
+ {products.map((product, index) => {
+ return ;
+ })}
+
+
+ '}
+ pageCount={Math.ceil(inventory.length / 12)}
+ breakLabel={'...'}
+ onPageChange={handlePageClick}
+ containerClassName={Styles.pagination}
+ pageClassName={Styles.pagination__item}
+ previousClassName={Styles.pagination__item}
+ nextClassName={Styles.pagination__item}
+ activeClassName={Styles.pagination__active}
+ />
+
+ {viewModal ?
: ''}
+
+ );
+}
diff --git a/src/pages/shopcart/ShopCart.module.css b/src/pages/shopcart/ShopCart.module.css
new file mode 100644
index 0000000..3bf3a51
--- /dev/null
+++ b/src/pages/shopcart/ShopCart.module.css
@@ -0,0 +1,85 @@
+.cartContainer {
+ display: grid;
+ grid-template-columns: 1fr 540px;
+ min-height: calc(100vh - 72px);
+}
+
+.resumeContainer {
+ box-shadow: 0px 4px 16px var(--box-shadows);
+ min-height: calc(100vh - 72px);
+ padding: 8px 24px;
+}
+
+.cart__items {
+ padding: 8px 16px;
+ height: max-content;
+}
+
+.cart__title {
+ text-align: center;
+ margin: 16px;
+ text-transform: uppercase;
+ color: var(--accented-green);
+}
+
+.cart__subtitle {
+ color: var(--texts);
+}
+
+.cart__grid {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.containerprice {
+ margin: 0px;
+ padding: 50px;
+}
+
+.ResumeTitle {
+ text-align: center;
+ margin: 16px;
+ text-transform: uppercase;
+ color: var(#4e4e4e);
+}
+
+.subtotal {
+ display: flex;
+ justify-content: space-between;
+ color: var(--texts-light);
+ margin-block: 10px;
+ font-size: var(--sm);
+}
+
+.total {
+ display: flex;
+ justify-content: space-between;
+ margin-block: 10px;
+ font-size: var(--sm);
+ font-weight: bold;
+ color: var(--texts);
+}
+
+.iva {
+ font-size: var(--sm);
+ color: var(--texts-light);
+}
+
+.dialog__button {
+ display: block;
+ width: 100%;
+ margin-block: 16px 12px;
+ font-size: var(--sm);
+ background-color: var(--accented-green);
+ padding: 0.8em;
+ outline: none;
+ color: white;
+ border: 1px solid transparent;
+ border-radius: 12px;
+ cursor: pointer;
+}
+
+.pregunta {
+ color: var(--accented-green);
+}
diff --git a/src/pages/shopcart/ShopCart.tsx b/src/pages/shopcart/ShopCart.tsx
new file mode 100644
index 0000000..fb5e539
--- /dev/null
+++ b/src/pages/shopcart/ShopCart.tsx
@@ -0,0 +1,116 @@
+import Styles from './ShopCart.module.css';
+import { Link, useNavigate } from 'react-router-dom';
+import { ShopCartPageRow } from './ShopCartPageRow/ShopCartPageRow';
+import { useContext, useEffect } from 'react';
+import { SessionContext } from '../../context/SessionContext';
+
+import { toast } from 'react-toastify';
+
+export function ShopCart() {
+ const { isSessionLoading, isLoggedIn, cart, makeOrder } = useContext(SessionContext);
+ const navigate = useNavigate();
+
+ // Redirect to home if is not logged in
+ useEffect(() => {
+ if (!isSessionLoading && !isLoggedIn) {
+ // Show an information alert
+ toast.warn('Please, log in to manage your cart', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+
+ // Redirect to heme because there is an active session
+ navigate('/');
+ }
+ }, [isSessionLoading, isLoggedIn]);
+
+ const GetCartTotal = () => {
+ const price = cart.reduce((acc, curr) => {
+ return acc + curr.price * curr.quantity;
+ }, 0);
+
+ return price.toFixed(2);
+ };
+
+ const GetCartTotalIva = () => {
+ const price = cart.reduce((acc, curr) => {
+ return acc + curr.price * curr.quantity * 0.19 + curr.price * curr.quantity;
+ }, 0);
+
+ return price.toFixed(2);
+ };
+
+ return (
+
+
+ Esta es tu cesta de la compra
+ {cart.length} Products:
+
+ {cart.map((item, index) => {
+ return ;
+ })}
+
+
+
+ Resumen de tu pedido:
+
+
+
SUBTOTAL:
+
{GetCartTotal()} €
+
+
+
TOTAL:
+
{GetCartTotalIva()} €
+
+
(Iva incluído)
+
+ {
+ const process = async () => {
+ const done = await makeOrder();
+
+ if (done) {
+ toast.success('Order was created successfully', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ } else {
+ toast.error('Unable to create a new order. Try again.', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ }
+ };
+
+ // Use the function if there are something in the cart
+ if (cart.length > 0) {
+ process();
+ } else {
+ toast.warn(
+ 'Your cart is empty, please, add some product before trying to create a new order',
+ {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ },
+ );
+ }
+ }}
+ >
+ Realizar pedido
+
+
+ Añadir más productos al carrito
+
+
+
+ );
+}
diff --git a/src/pages/shopcart/ShopCartPageRow/ShopCartPageRow.module.css b/src/pages/shopcart/ShopCartPageRow/ShopCartPageRow.module.css
new file mode 100644
index 0000000..5e5d3e1
--- /dev/null
+++ b/src/pages/shopcart/ShopCartPageRow/ShopCartPageRow.module.css
@@ -0,0 +1,50 @@
+.product {
+ display: grid;
+ margin-inline-start: 12px;
+ grid-template-columns: 0.8fr 2fr 2fr 1fr 1fr;
+ gap: 12px;
+}
+
+.product__image {
+ width: 100%;
+ object-fit: contain;
+}
+
+.product__title {
+ font-size: var(--lg);
+ color: var(--texts);
+ margin-block-end: 8px;
+}
+
+.product__units {
+ color: var(--texts-light);
+}
+
+.product__details,
+.product__col {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
+
+.product__group {
+ display: flex;
+ align-items: center;
+}
+
+.product__col {
+ align-items: center;
+}
+
+.product__price {
+ font-size: var(--sm);
+ font-weight: bolder;
+ color: var(--texts);
+}
+
+.product__input {
+ padding: 0.8em;
+ margin-inline: 16px;
+ border: 1px solid var(--texts-light);
+ border-radius: 8px;
+}
diff --git a/src/pages/shopcart/ShopCartPageRow/ShopCartPageRow.tsx b/src/pages/shopcart/ShopCartPageRow/ShopCartPageRow.tsx
new file mode 100644
index 0000000..536aa92
--- /dev/null
+++ b/src/pages/shopcart/ShopCartPageRow/ShopCartPageRow.tsx
@@ -0,0 +1,81 @@
+import Styles from './ShopCartPageRow.module.css';
+import { useContext } from 'react';
+import { SessionContext } from '../../../context/SessionContext';
+import { FiTrash2 } from 'react-icons/fi';
+import { toast } from 'react-toastify';
+
+interface IProduct {
+ id: string;
+ image: string;
+ name: string;
+ units: string;
+ quantity: number;
+ price: number;
+}
+
+interface IProps {
+ product: IProduct;
+}
+
+export function ShopCartPageRow(props: IProps) {
+ const { removeFromCart, updateCart } = useContext(SessionContext);
+
+ const HandleRemoveFromCart = async () => {
+ const wasDeleted = await removeFromCart(props.product.id);
+
+ if (wasDeleted) {
+ toast.success(`Successfully remove ${props.product.name} from the cart`, {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ } else {
+ toast.error(`Unable to remove ${props.product.name} from the cart. Try again.`, {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ }
+ };
+
+ return (
+
+
+
+
{props.product.name}
+
Unidades: {props.product.units}
+
+
+
+ Cantidad
+
+ {
+ // Update value on session
+ const value = e.target.value;
+ updateCart({ id: props.product.id, amount: value });
+ }}
+ />
+
+
+
{props.product.price}€
+
+
+ {
+ HandleRemoveFromCart();
+ }}
+ />
+
+
+ );
+}
diff --git a/src/pages/signup/Signup.tsx b/src/pages/signup/Signup.tsx
new file mode 100644
index 0000000..d52762e
--- /dev/null
+++ b/src/pages/signup/Signup.tsx
@@ -0,0 +1,116 @@
+import { useContext, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { SessionContext } from '../../context/SessionContext';
+
+import { SignupService } from '../../services/user.services';
+import { ISignupPayload } from '../../interfaces/interfaces.services';
+import { DynamicForm } from '../../components/dynamicForm/DynamicForm';
+
+import { toast } from 'react-toastify';
+
+export function Signup() {
+ const { isLoggedIn, isSessionLoading } = useContext(SessionContext);
+ const navigate = useNavigate();
+
+ // Redirect to heme if is logged in
+ useEffect(() => {
+ if (!isSessionLoading && isLoggedIn) {
+ // Show an information alert
+ toast.warn('You already have an active session', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ }
+ }, [isSessionLoading, isLoggedIn]);
+
+ const HandleSignupSubmit = async (payload: ISignupPayload) => {
+ // Get response from back-end
+ const response = await SignupService(payload);
+ const data = response?.data;
+
+ if (response?.status !== 200) {
+ // Shows an error alert
+ toast.error(data.message, {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+ } else {
+ toast.success('User was created successfully', {
+ position: 'top-right',
+ autoClose: 2500,
+ pauseOnHover: true,
+ theme: 'light',
+ });
+
+ navigate('/login');
+ }
+ };
+
+ const signupRules = [
+ {
+ name: 'password',
+ done: false,
+ regexp: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$!%*#?&/%])[A-Za-z\d$!%*#?&/%]{8,}$/,
+ message:
+ 'Password must have minimal length of 8 characters, contains at least one number, one letter and one special character',
+ },
+ {
+ name: 'password2',
+ done: false,
+ regexp: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[$!%*#?&/%])[A-Za-z\d$!%*#?&/%]{8,}$/,
+ message:
+ 'Password must have minimal length of 8 characters, contains at least one number, one letter and one special character',
+ },
+ ];
+
+ const signupFields = [
+ {
+ label: 'First name',
+ name: 'firstName',
+ type: 'text',
+ placeholder: 'Foo',
+ minlength: 5,
+ },
+ {
+ label: 'Last name',
+ name: 'lastName',
+ type: 'text',
+ placeholder: 'Bar',
+ minlength: 5,
+ },
+ {
+ label: 'Email',
+ name: 'email',
+ type: 'email',
+ placeholder: 'foo@bar.com',
+ },
+ {
+ label: 'Password',
+ name: 'password',
+ type: 'password',
+ placeholder: '********',
+ minlength: 8,
+ },
+ {
+ label: 'Confirm Password',
+ name: 'password2',
+ type: 'password',
+ placeholder: '********',
+ minlength: 8,
+ },
+ ];
+
+ return (
+
+ );
+}
diff --git a/src/services/products.service.ts b/src/services/products.service.ts
new file mode 100644
index 0000000..5f9da9a
--- /dev/null
+++ b/src/services/products.service.ts
@@ -0,0 +1,31 @@
+import axios from 'axios';
+import { GLOBALS } from '../config/config';
+
+export const getProducts = async () => {
+ const response = await axios.get(`${GLOBALS.API_HOST}/api/products`);
+ return response.data;
+};
+
+export const getProductsFiltrated = async (from: number, to: number, criteria: string) => {
+ const response = await axios.post(`${GLOBALS.API_HOST}/api/products/filter`, {
+ from: from,
+ to: to,
+ search_criteria: criteria,
+ });
+ return response.data;
+};
+
+// export const getPageproducts = async (page: number) => {
+// const response = await axios.get(`${GLOBALS.API_HOST}/api/products/${page}`);
+// return response.data;
+// };
+
+export const getProductImage = async (serial: number) => {
+ const response = await axios.get(`${GLOBALS.API_HOST}/api/products/image/${serial}`);
+ return response.data;
+};
+
+export const GetProductImageFromEndpointService = async (endpoint: string) => {
+ const response = await axios.get(`${GLOBALS.API_HOST}/api${endpoint}`);
+ return response.data;
+};
diff --git a/src/services/session.services.ts b/src/services/session.services.ts
new file mode 100644
index 0000000..dd42647
--- /dev/null
+++ b/src/services/session.services.ts
@@ -0,0 +1,225 @@
+import axios from 'axios';
+import { GLOBALS } from '../config/config';
+import { ILoginPayload } from '../interfaces/interfaces.services';
+
+// Create the login request to the API to obtain the
+// session tokens
+// withCredentials field is required
+export const LoginService = async (payload: ILoginPayload) => {
+ try {
+ const response = await axios.post(`${GLOBALS.API_HOST}/api/session/login`, payload, {
+ withCredentials: true,
+ });
+
+ return response;
+ } catch (err) {
+ // Return the body of the error
+ if (axios.isAxiosError(err)) return err.response;
+ // Creates an empty error
+ else return new axios.AxiosError().response;
+ }
+};
+
+// Get expired tokens
+export const LogoutService = async (it: number): Promise => {
+ if (it > 2) return false;
+
+ try {
+ await axios.delete(`${GLOBALS.API_HOST}/api/session/logout`, {
+ withCredentials: true,
+ });
+ return true;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await LogoutService(++it);
+ }
+
+ return false;
+ }
+ return false;
+ }
+};
+
+// Get a new access-token
+export const RefreshTokenService = async (): Promise => {
+ try {
+ await axios.get(`${GLOBALS.API_HOST}/api/session/refresh`, {
+ withCredentials: true,
+ });
+
+ return true;
+ } catch (err) {
+ return false;
+ }
+};
+
+// Recover the session user data from the api
+// from the access token sended as a cookie
+// withCredentials field is required
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const WhoamiService = async (it: number): Promise => {
+ if (it > 2) return new axios.AxiosError().response;
+
+ try {
+ const response = await axios.get(`${GLOBALS.API_HOST}/api/session/whoami`, {
+ withCredentials: true,
+ });
+
+ return response;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ // Error caused because of no access-token provided
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await WhoamiService(++it); // Try one more time
+ }
+
+ return err.response;
+ }
+ return new axios.AxiosError().response;
+ }
+};
+
+// Get user cart from api
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const GetCartService = async (it: number): Promise => {
+ if (it > 2) return new axios.AxiosError().response;
+
+ try {
+ const response = await axios.get(`${GLOBALS.API_HOST}/api/cart`, {
+ withCredentials: true,
+ });
+ return response;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ // Error caused because of no access-token provided
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await GetCartService(++it); // Try one more time
+ }
+
+ return err.response;
+ }
+ return new axios.AxiosError().response;
+ }
+};
+
+// Ger user favorites on load
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const GetFavoritesService = async (it: number): Promise => {
+ if (it > 2) return new axios.AxiosError().response;
+
+ try {
+ const response = await axios.get(`${GLOBALS.API_HOST}/api/user/favorites/list`, {
+ withCredentials: true,
+ });
+
+ return response;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ // Error caused because of no access-token provided
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await GetFavoritesService(++it); // Try one more time
+ }
+
+ return err.response;
+ }
+ return new axios.AxiosError().response;
+ }
+};
+
+// Add to user cart
+export const AddToCartService = async (it: number, id: string): Promise => {
+ if (it > 2) return false;
+
+ try {
+ const payload = { id };
+ const response = await axios.post(`${GLOBALS.API_HOST}/api/cart`, payload, {
+ withCredentials: true,
+ });
+
+ return response.status === 200 ? true : false;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await AddToCartService(++it, id);
+ }
+ return false;
+ }
+ return false;
+ }
+};
+
+// Add to favorites
+export const AddToFavoritesService = async (it: number, id: string): Promise => {
+ if (it > 2) return false;
+
+ try {
+ const payload = { id };
+ const response = await axios.post(`${GLOBALS.API_HOST}/api/user/favorites`, payload, {
+ withCredentials: true,
+ });
+
+ return response.status === 200 ? true : false;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await AddToFavoritesService(++it, id);
+ }
+ return false;
+ }
+ return false;
+ }
+};
+
+// Remove item from user cart
+export const RemoveFromCartService = async (it: number, id: string): Promise => {
+ if (it > 2) return false;
+
+ try {
+ const response = await axios.delete(`${GLOBALS.API_HOST}/api/cart/${id}`, {
+ withCredentials: true,
+ });
+
+ return response.status === 200 ? true : false;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ // Error caused because of no access-token provided
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await RemoveFromCartService(++it, id); // Try one more time
+ }
+ return false;
+ }
+
+ return false;
+ }
+};
+
+// Remove from favorites
+export const RemoveFromFavoritesService = async (it: number, id: string): Promise => {
+ if (it > 2) return false;
+
+ try {
+ const response = await axios.delete(`${GLOBALS.API_HOST}/api/user/favorites/${id}`, {
+ withCredentials: true,
+ });
+
+ return response.status === 200 ? true : false;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await RemoveFromFavoritesService(++it, id);
+ }
+ return false;
+ }
+ return false;
+ }
+};
diff --git a/src/services/shop.services.ts b/src/services/shop.services.ts
new file mode 100644
index 0000000..55e8286
--- /dev/null
+++ b/src/services/shop.services.ts
@@ -0,0 +1,59 @@
+import axios from 'axios';
+import { GLOBALS } from '../config/config';
+import { RefreshTokenService } from './session.services';
+import { IUpdateCartPayload } from '../interfaces/interfaces.services';
+
+// Make an order from the items on cart
+export const OrderService = async (it: number): Promise => {
+ if (it > 2) return false;
+
+ try {
+ await axios.post(
+ `${GLOBALS.API_HOST}/api/order`,
+ {},
+ {
+ withCredentials: true,
+ },
+ );
+
+ return true;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await OrderService(++it);
+ }
+ return false;
+ }
+ return false;
+ }
+};
+
+// Update amount for items on cart
+export const UpdateCartItemAmount = async (
+ it: number,
+ payload: IUpdateCartPayload,
+): Promise => {
+ if (it > 2) return false;
+
+ try {
+ await axios.put(
+ `${GLOBALS.API_HOST}/api/cart`,
+ { ...payload, amount: parseInt(payload.amount) },
+ {
+ withCredentials: true,
+ },
+ );
+
+ return true;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await UpdateCartItemAmount(++it, payload);
+ }
+ return false;
+ }
+ return false;
+ }
+};
diff --git a/src/services/user.services.ts b/src/services/user.services.ts
new file mode 100644
index 0000000..1052686
--- /dev/null
+++ b/src/services/user.services.ts
@@ -0,0 +1,41 @@
+import axios from 'axios';
+import { GLOBALS } from '../config/config';
+import { ISignupPayload } from '../interfaces/interfaces.services';
+import { RefreshTokenService } from './session.services';
+
+// Create a new user on database
+export const SignupService = async (payload: ISignupPayload) => {
+ try {
+ const response = await axios.post(`${GLOBALS.API_HOST}/api/user`, payload);
+
+ return response;
+ } catch (err) {
+ if (axios.isAxiosError(err)) return err.response;
+ return new axios.AxiosError().response;
+ }
+};
+
+// Get detailed favorites (For /favorites route)
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const GetDetailedFavoritesService = async (it: number): Promise => {
+ if (it > 2) return new axios.AxiosError().response;
+
+ try {
+ const response = await axios.get(`${GLOBALS.API_HOST}/api/user/favorites/detailed`, {
+ withCredentials: true,
+ });
+
+ return response;
+ } catch (err) {
+ if (axios.isAxiosError(err)) {
+ // Error caused because of no access-token provided
+ if (err.response?.status === 403) {
+ await RefreshTokenService();
+ return await GetDetailedFavoritesService(++it); // Try one more time
+ }
+
+ return err.response;
+ }
+ return new axios.AxiosError().response;
+ }
+};
diff --git a/src/templates/user.ts b/src/templates/user.ts
new file mode 100644
index 0000000..6ed6883
--- /dev/null
+++ b/src/templates/user.ts
@@ -0,0 +1,8 @@
+import { IUser } from '../interfaces/interfaces';
+
+export const UserTemplate: IUser = {
+ id: 0,
+ email: 'foo@bar.com',
+ firstname: 'foo',
+ lastname: 'bar',
+};
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..9fde9cf
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,9 @@
+///
+
+declare module '*.css';
+
+interface ImportMeta {
+ env: {
+ API_HOST: 'localhost:3030';
+ };
+}