diff --git a/web/package-lock.json b/web/package-lock.json
index 202bff49067475b51b94053c6877e63a1c875fac..8d194350e6765bdc34341ef8a3abe07c7899b848 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -8,6 +8,8 @@
"name": "web",
"version": "0.1.0",
"dependencies": {
+ "@fortawesome/free-solid-svg-icons": "^6.5.1",
+ "@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -15,11 +17,18 @@
"@types/node": "^16.18.87",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
+ "axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hook-form": "^7.51.2",
+ "react-pro-sidebar": "^1.1.0",
+ "react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
+ },
+ "devDependencies": {
+ "tailwindcss": "^3.4.1"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2288,6 +2297,163 @@
"postcss-selector-parser": "^6.0.10"
}
},
+ "node_modules/@emotion/babel-plugin": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz",
+ "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.16.7",
+ "@babel/runtime": "^7.18.3",
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/serialize": "^1.1.2",
+ "babel-plugin-macros": "^3.1.0",
+ "convert-source-map": "^1.5.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-root": "^1.1.0",
+ "source-map": "^0.5.7",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@emotion/babel-plugin/node_modules/source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@emotion/cache": {
+ "version": "11.11.0",
+ "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz",
+ "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/sheet": "^1.2.2",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "stylis": "4.2.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.9.1",
+ "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz",
+ "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ=="
+ },
+ "node_modules/@emotion/is-prop-valid": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
+ "integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
+ "dependencies": {
+ "@emotion/memoize": "^0.8.1"
+ }
+ },
+ "node_modules/@emotion/memoize": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
+ "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
+ },
+ "node_modules/@emotion/react": {
+ "version": "11.11.4",
+ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.4.tgz",
+ "integrity": "sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/cache": "^11.11.0",
+ "@emotion/serialize": "^1.1.3",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1",
+ "@emotion/weak-memoize": "^0.3.1",
+ "hoist-non-react-statics": "^3.3.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/serialize": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.4.tgz",
+ "integrity": "sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==",
+ "dependencies": {
+ "@emotion/hash": "^0.9.1",
+ "@emotion/memoize": "^0.8.1",
+ "@emotion/unitless": "^0.8.1",
+ "@emotion/utils": "^1.2.1",
+ "csstype": "^3.0.2"
+ }
+ },
+ "node_modules/@emotion/sheet": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz",
+ "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA=="
+ },
+ "node_modules/@emotion/styled": {
+ "version": "11.11.5",
+ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.5.tgz",
+ "integrity": "sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "@emotion/babel-plugin": "^11.11.0",
+ "@emotion/is-prop-valid": "^1.2.2",
+ "@emotion/serialize": "^1.1.4",
+ "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
+ "@emotion/utils": "^1.2.1"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.0.0-rc.0",
+ "react": ">=16.8.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
+ "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ=="
+ },
+ "node_modules/@emotion/use-insertion-effect-with-fallbacks": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
+ "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
+ "peerDependencies": {
+ "react": ">=16.8.0"
+ }
+ },
+ "node_modules/@emotion/utils": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz",
+ "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg=="
+ },
+ "node_modules/@emotion/weak-memoize": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz",
+ "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww=="
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -2381,6 +2547,52 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz",
+ "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==",
+ "hasInstallScript": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz",
+ "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==",
+ "hasInstallScript": true,
+ "peer": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz",
+ "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.5.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/react-fontawesome": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz",
+ "integrity": "sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6",
+ "react": ">=16.3"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@@ -3338,6 +3550,23 @@
}
}
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.15.3",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
+ "integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -5223,6 +5452,29 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
+ "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axios/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/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@@ -5923,6 +6175,11 @@
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz",
"integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ=="
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
+ },
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@@ -8274,6 +8531,11 @@
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
}
},
+ "node_modules/find-root": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
+ "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -8308,9 +8570,9 @@
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
},
"node_modules/follow-redirects": {
- "version": "1.15.5",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
- "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
+ "version": "1.15.6",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
+ "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
"funding": [
{
"type": "individual",
@@ -8938,6 +9200,19 @@
"he": "bin/he"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/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=="
+ },
"node_modules/hoopy": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
@@ -14522,6 +14797,11 @@
"node": ">= 0.10"
}
},
+ "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/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@@ -14805,11 +15085,41 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
},
+ "node_modules/react-hook-form": {
+ "version": "7.51.2",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.2.tgz",
+ "integrity": "sha512-y++lwaWjtzDt/XNnyGDQy6goHskFualmDlf+jzEZvjvz6KWDf7EboL7pUvRCzPTJd0EOPpdekYaQLEvvG6m6HA==",
+ "engines": {
+ "node": ">=12.22.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
+ "node_modules/react-pro-sidebar": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/react-pro-sidebar/-/react-pro-sidebar-1.1.0.tgz",
+ "integrity": "sha512-rdRJ4PeMsqWq9n69AmF6et6qCbhCF1KEBgjAH8vIiLxE1k5fMxtRYo0k4asxW8qpIH6sqahiMxrxVVoObv8orQ==",
+ "dependencies": {
+ "@emotion/react": "^11.10.5",
+ "@emotion/styled": "^11.10.5",
+ "@popperjs/core": "^2.11.6",
+ "classnames": "^2.3.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
@@ -14818,6 +15128,36 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
+ "integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
+ "dependencies": {
+ "@remix-run/router": "1.15.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.22.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
+ "integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
+ "dependencies": {
+ "@remix-run/router": "1.15.3",
+ "react-router": "6.22.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -16219,6 +16559,11 @@
"postcss": "^8.2.15"
}
},
+ "node_modules/stylis": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
+ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
+ },
"node_modules/sucrase": {
"version": "3.35.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
diff --git a/web/package.json b/web/package.json
index 600a7e754f9e7e10c3ccd6f00eef03f0a9834790..f36efbd589ff32f2155b6d383a848966e61fb095 100644
--- a/web/package.json
+++ b/web/package.json
@@ -3,6 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
+ "@fortawesome/free-solid-svg-icons": "^6.5.1",
+ "@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
@@ -10,8 +12,12 @@
"@types/node": "^16.18.87",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
+ "axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hook-form": "^7.51.2",
+ "react-pro-sidebar": "^1.1.0",
+ "react-router-dom": "^6.22.3",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
@@ -39,5 +45,8 @@
"last 1 firefox version",
"last 1 safari version"
]
+ },
+ "devDependencies": {
+ "tailwindcss": "^3.4.1"
}
}
diff --git a/web/src/App.tsx b/web/src/App.tsx
index a53698aab3c66049c61980112dd0109dd2cd0845..8dff12f1d12ddbbf9d2a12ea5179b98385885076 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -1,24 +1,18 @@
-import React from 'react';
-import logo from './logo.svg';
+import { RouterProvider } from 'react-router-dom';
import './App.css';
+import { AuthContextProvider } from './context/auth_context';
+import { MessageContextProvider } from './context/message_context';
+import { router } from './router/router';
+
function App() {
return (
);
}
diff --git a/web/src/components/admin_panel_navbar/admin_navbar.tsx b/web/src/components/admin_panel_navbar/admin_navbar.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..424bd0633d14451ce363068a75b14018fdda6fbf
--- /dev/null
+++ b/web/src/components/admin_panel_navbar/admin_navbar.tsx
@@ -0,0 +1,45 @@
+import { useState } from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faSignOut, faUser } from "@fortawesome/free-solid-svg-icons";
+import { useAuth } from "../../context/auth_context";
+import { Link } from "react-router-dom";
+import './assets/styles/style.css';
+
+export const AdminPanelNavBar = () => {
+ const {user, logout} = useAuth();
+ const [toggle, setToggle] = useState(false);
+
+ const handleLogout = () => {
+ logout();
+ };
+
+ return (
+
+
+
setToggle(!toggle)}/>
+ {toggle &&
+
+
+
+
+
Superadmin
+
+
+
+
+
+
Editar cuenta
+
+
+
+
+
Cerrar sesión
+
+
+
+ }
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/components/admin_panel_navbar/assets/images/Admin-595b40b65ba036ed117d36fe.png b/web/src/components/admin_panel_navbar/assets/images/Admin-595b40b65ba036ed117d36fe.png
new file mode 100644
index 0000000000000000000000000000000000000000..f2e44e0c59cb9d29b25a8c922f4ea1fdc3cab308
Binary files /dev/null and b/web/src/components/admin_panel_navbar/assets/images/Admin-595b40b65ba036ed117d36fe.png differ
diff --git a/web/src/components/admin_panel_navbar/assets/styles/style.css b/web/src/components/admin_panel_navbar/assets/styles/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..0762a6709c9e7e0d467a5cf68bd373d6b71dfb5c
--- /dev/null
+++ b/web/src/components/admin_panel_navbar/assets/styles/style.css
@@ -0,0 +1,90 @@
+:root {
+ --shadow: 0px 2px 8px 0px gray;
+}
+
+.navbar{
+ height: 40px;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: var(--shadow);
+
+}
+
+.profile{
+ position: absolute;
+ right: 20px;
+}
+
+.user-pic{
+ width: 30px;
+ border-radius: 50%;
+ cursor: pointer;
+ background: white;
+}
+
+.profile-sub-menu-wrap{
+ position: absolute;
+ top: 100%;
+ right: 10%;
+ max-width: 400px;
+ overflow: hidden;
+ transition: max-height 0.5s;
+}
+
+.sub-menu{
+ background: white;
+ padding: 10px;
+ margin: 10px;
+}
+
+.user-info{
+ display: flex;
+ align-items: center;
+}
+
+.user-info img{
+ width: 60px;
+ margin-right: 15px;
+ border-radius: 50%;
+}
+
+.user-info h3{
+ font-weight: 500;
+}
+
+.sub-menu hr{
+ border: 0;
+ height: 1px;
+ width: 100%;
+ background: #ccc ;
+ margin: 15px 0 10px;
+}
+
+.sub-menu-link{
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ color: #525252;
+}
+
+.sub-menu-link:hover .sub-menu-link-icon{
+ transform: scale(1.1);
+}
+
+.sub-menu-link:hover p{
+ font-weight: 600;
+}
+
+.sub-menu-link p{
+ user-select: none;
+}
+
+.sub-menu-link .sub-menu-link-icon{
+ width: 20px;
+ background: #E5E5E5;
+ border-radius: 50%;
+ padding: 8px;
+ margin-right: 15px;
+}
\ No newline at end of file
diff --git a/web/src/components/sa_panel_admin/sa_panel_admin_screen/sa_panel_admin_screen.tsx b/web/src/components/sa_panel_admin/sa_panel_admin_screen/sa_panel_admin_screen.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b91f596188474ba0831d775a137fc84fbb862ac0
--- /dev/null
+++ b/web/src/components/sa_panel_admin/sa_panel_admin_screen/sa_panel_admin_screen.tsx
@@ -0,0 +1,8 @@
+
+export const SuperadminPanelAdminScreen = () => {
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/components/sa_panel_town/sa_panel_town_screen/css/styles.css b/web/src/components/sa_panel_town/sa_panel_town_screen/css/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..2713080dda323b253a843a595638d986d34072a9
--- /dev/null
+++ b/web/src/components/sa_panel_town/sa_panel_town_screen/css/styles.css
@@ -0,0 +1,20 @@
+.sa_panel_town_content{
+ background: gray;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+.town_panel_header{
+ display: flex;
+ width: 100%;
+ align-items: center;
+ justify-content: center;
+}
+
+.town_add_btn{
+ position: absolute;
+ top: 50%;
+ right: 5px;
+}
\ No newline at end of file
diff --git a/web/src/components/sa_panel_town/sa_panel_town_screen/sa_panel_town_screen.tsx b/web/src/components/sa_panel_town/sa_panel_town_screen/sa_panel_town_screen.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..dc7bab0f97a32cc17ce7c26352c254d2283f7e46
--- /dev/null
+++ b/web/src/components/sa_panel_town/sa_panel_town_screen/sa_panel_town_screen.tsx
@@ -0,0 +1,15 @@
+import './css/styles.css'
+
+export const SuperadminPanelTownScreen = () => {
+ return (
+
+
+ Administrar pueblos mágicos
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/components/sidebar_header/assets/css/styles.css b/web/src/components/sidebar_header/assets/css/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..f47166b7c4d5fa5d20181f23a77c6be4905e9d06
--- /dev/null
+++ b/web/src/components/sidebar_header/assets/css/styles.css
@@ -0,0 +1,11 @@
+.sidebar_header {
+ height: 64px;
+ min-height: 64px;
+ display: flex;
+ align-items: center;
+ padding: 0 20px;
+}
+
+.sidebar_header_content{
+
+}
\ No newline at end of file
diff --git a/web/src/components/sidebar_header/sidebar_header.tsx b/web/src/components/sidebar_header/sidebar_header.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f23c496b1fe84e6e2c2b3fd14bd9ddc697deb734
--- /dev/null
+++ b/web/src/components/sidebar_header/sidebar_header.tsx
@@ -0,0 +1,18 @@
+import { UserRole } from '../../constants/roles';
+import { useAuth } from '../../context/auth_context';
+import './assets/css/styles.css';
+
+export const SidebarHeader = () => {
+ const {user} = useAuth();
+ if(!user){
+ return null;
+ }
+
+ return (
+
+
+
{user.role==UserRole.SUPERADMIN ? 'Superadmin': 'Admin'} Panel
+
+
+ );
+}
\ No newline at end of file
diff --git a/web/src/constants/api_url.ts b/web/src/constants/api_url.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b712d33928a6ffa71f9619a88dfe2a43330182a0
--- /dev/null
+++ b/web/src/constants/api_url.ts
@@ -0,0 +1 @@
+export const APIUrl: String = "http://localhost:3005";
\ No newline at end of file
diff --git a/web/src/constants/roles.ts b/web/src/constants/roles.ts
new file mode 100644
index 0000000000000000000000000000000000000000..02cddf704502cf502565b1bb76d13ecee2d0b2b2
--- /dev/null
+++ b/web/src/constants/roles.ts
@@ -0,0 +1,7 @@
+export enum UserRole {
+ ADMIN = "admin",
+ SUPERADMIN = "superadmin",
+}
+
+export const SUPERADMIN_ROLES = [UserRole.SUPERADMIN];
+export const ADMIN_ROLES = [UserRole.ADMIN, UserRole.SUPERADMIN];
\ No newline at end of file
diff --git a/web/src/context/auth_context.tsx b/web/src/context/auth_context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2648700def5b1f352a261097a596cfdd34c1e7b5
--- /dev/null
+++ b/web/src/context/auth_context.tsx
@@ -0,0 +1,68 @@
+import { UserEntity } from "../infraestructure/entities/user";
+import axios from "axios";
+import { UserRole } from "../constants/roles";
+import { ReactNode, createContext, useContext, useEffect, useState } from "react";
+
+type AuthContextType = {
+ user: UserEntity | null;
+ login: (user: UserEntity, token: string) => void;
+ logout: () => void;
+};
+
+const AuthContext = createContext({
+ user: null,
+ login: () => {},
+ logout: () => {}
+});
+
+type AuthContextProviderProps = {
+ children: ReactNode;
+}
+
+export const AuthContextProvider = ({children} : AuthContextProviderProps) => {
+ const [user, setUser] = useState(null);
+ const saveSession = async (user: UserEntity, token: string) => {
+ setUser(user);
+ localStorage.setItem("token",token);
+ localStorage.setItem("user",JSON.stringify(user));
+ axios.defaults.headers.common["Authorization"] = `${token}`;
+ }
+
+ const deleteSession = async () => {
+ setUser(null);
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ axios.defaults.headers.common["Authorization"] = "";
+ }
+
+ const login = async (user: UserEntity, token: string) => {
+ await saveSession(user, token);
+ }
+
+ const logout = async () => {
+ await deleteSession();
+ }
+
+ const checkSession = async () => {
+ const token = localStorage.getItem("token");
+ const user = localStorage.getItem("user");
+ if(token && user){
+ const sessionUser = JSON.parse(user);
+ await saveSession({email: sessionUser.email as string, role: sessionUser.role as UserRole}, token);
+ }
+ }
+
+ useEffect(() => {
+ checkSession();
+ }, []);
+
+ const value = {user, login, logout};
+ return {children}
+};
+
+export const useAuth = () => {
+ if(AuthContext == undefined){
+ throw new Error("useAuth debe se usado dentro de un AuthContextProvider");
+ }
+ return useContext(AuthContext);
+};
\ No newline at end of file
diff --git a/web/src/context/message_context.tsx b/web/src/context/message_context.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1ce309bec77a5cb93eb182b374430ac90cb8e8bd
--- /dev/null
+++ b/web/src/context/message_context.tsx
@@ -0,0 +1,47 @@
+import { createContext, ReactNode, useContext, useState } from "react";
+
+type MessageContextType = {
+ isVisible: boolean;
+ message: string;
+ showMessage: (message: string) => void;
+ hideMessage: () => void;
+}
+
+const MessageContext = createContext({
+ isVisible: false,
+ message: "",
+ showMessage: () => {},
+ hideMessage: () => {}
+});
+
+type MessageContextProviderProps = {
+ children: ReactNode;
+}
+
+export const MessageContextProvider = ({children}: MessageContextProviderProps) => {
+ const [isVisible, setIsVisible] = useState(false);
+ const [message, setMessage] = useState("");
+ const showMessage = (message: string) => {
+ setIsVisible(true);
+ setMessage(message);
+ setTimeout(() => {
+ setIsVisible(false);
+ setMessage("");
+ }, 3000);
+ };
+
+ const hideMessage = () => {
+ setIsVisible(false);
+ setMessage("");
+ };
+
+ const value = {isVisible, message, showMessage, hideMessage};
+ return {children}
+}
+
+export const useMessage = () => {
+ if(MessageContext == undefined){
+ throw new Error("useMessage debe se usado dentro de un MessageContextProvider");
+ }
+ return useContext(MessageContext);
+};
\ No newline at end of file
diff --git a/web/src/data/datasources/prod/login_datasource.ts b/web/src/data/datasources/prod/login_datasource.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c2d7f535a1381fe04aef54278c4b8ce15fb32ed3
--- /dev/null
+++ b/web/src/data/datasources/prod/login_datasource.ts
@@ -0,0 +1,30 @@
+import { LoginDatasourceInf } from "../../../infraestructure/datasources/login_datasource";
+import { LoginFormValues } from "../../../infraestructure/entities/login_form_values";
+import axios from "axios";
+import { LoggedInUser } from "../../../infraestructure/entities/user";
+import { LoggedInUserModel } from "../../models/prod/LoggedInUserModel";
+import { UserRole } from "../../../constants/roles";
+import { APIUrl } from "../../../constants/api_url";
+
+export class LoginDatasourceProd implements LoginDatasourceInf{
+ async getToken(form: LoginFormValues): Promise {
+ const {email, password} = form;
+ const {data} = await axios.post(
+ APIUrl + "/admin/signin",
+ {
+ email: email,
+ password: password,
+ }
+ );
+ const user: LoggedInUser = {
+ user: {
+ email: data.email,
+ name: data.name,
+ role: data.role === UserRole.SUPERADMIN ? UserRole.SUPERADMIN : UserRole.ADMIN,
+ },
+ token: data.token,
+ };
+
+ return user;
+ }
+}
\ No newline at end of file
diff --git a/web/src/data/models/prod/LoggedInUserModel.ts b/web/src/data/models/prod/LoggedInUserModel.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f73d45b187796cdfa6ae2c154a6d1b5a252db0c
--- /dev/null
+++ b/web/src/data/models/prod/LoggedInUserModel.ts
@@ -0,0 +1,6 @@
+export interface LoggedInUserModel {
+ email: string;
+ name: string;
+ role: string;
+ token: string;
+}
\ No newline at end of file
diff --git a/web/src/data/repositories/prod/login_repository.ts b/web/src/data/repositories/prod/login_repository.ts
new file mode 100644
index 0000000000000000000000000000000000000000..69c0694fa4ea1ac50aa3890d7c8aedc305f364c1
--- /dev/null
+++ b/web/src/data/repositories/prod/login_repository.ts
@@ -0,0 +1,13 @@
+import { LoginDatasourceInf } from "../../../infraestructure/datasources/login_datasource";
+import { LoginFormValues } from "../../../infraestructure/entities/login_form_values";
+import { LoggedInUser } from "../../../infraestructure/entities/user";
+import { LoginRepositoryInf } from "../../../infraestructure/repositories/login_repository";
+
+export class LoginRepositoryProd implements LoginRepositoryInf{
+ constructor(
+ private datasource: LoginDatasourceInf
+ ){}
+ async getToken(form: LoginFormValues): Promise {
+ return this.datasource.getToken(form);
+ }
+}
\ No newline at end of file
diff --git a/web/src/hooks/useLogin.tsx b/web/src/hooks/useLogin.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6a8ec16917269db00aac523e77ea0761715b1cba
--- /dev/null
+++ b/web/src/hooks/useLogin.tsx
@@ -0,0 +1,41 @@
+import { SubmitHandler, useForm } from "react-hook-form"
+import { LoginFormValues } from "../infraestructure/entities/login_form_values"
+import { LoginRepositoryProd } from "../data/repositories/prod/login_repository";
+import { LoginDatasourceProd } from "../data/datasources/prod/login_datasource";
+import { useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { useAuth } from "../context/auth_context";
+const loginDatasource = new LoginDatasourceProd();
+const loginRepository = new LoginRepositoryProd(loginDatasource);
+
+export const useLogin = () => {
+ const {login, user} = useAuth();
+ const navigate = useNavigate();
+ const {
+ register,
+ handleSubmit,
+ setError,
+ formState: {errors},
+ } = useForm();
+
+ const onSubmit: SubmitHandler = (data: LoginFormValues) => {
+ const authenticate = async () => {
+ try{
+ const {user, token } = await loginRepository.getToken(data);
+ await login(user, token);
+ navigate("/");
+ }catch(error: any){
+
+ }
+ }
+ authenticate();
+ }
+
+ useEffect(() => {
+ if(user) {
+ navigate("/");
+ }
+ }, []);
+
+ return { register, handleSubmit, onSubmit, errors };
+}
\ No newline at end of file
diff --git a/web/src/hooks/usePasswordVisibility.tsx b/web/src/hooks/usePasswordVisibility.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..23108a824cab3a048531926308d79f2c05edafc4
--- /dev/null
+++ b/web/src/hooks/usePasswordVisibility.tsx
@@ -0,0 +1,20 @@
+import { useState } from "react"
+
+export const usePasswoordVisibility = () => {
+ const [values, setValues] = useState({
+ showPassword: false,
+ });
+
+ const handleClickShowPassword = () => {
+ setValues({
+ ...values,
+ showPassword: !values.showPassword,
+ });
+ };
+
+ const handleMouseDownPassword = (event: any) => {
+ event.preventDefault();
+ };
+
+ return {values, handleClickShowPassword, handleMouseDownPassword};
+}
\ No newline at end of file
diff --git a/web/src/infraestructure/datasources/login_datasource.ts b/web/src/infraestructure/datasources/login_datasource.ts
new file mode 100644
index 0000000000000000000000000000000000000000..762def1d77f5e3db577ea99cb2f7e913d414ba8e
--- /dev/null
+++ b/web/src/infraestructure/datasources/login_datasource.ts
@@ -0,0 +1,6 @@
+import { LoginFormValues } from "../entities/login_form_values";
+import { LoggedInUser } from "../entities/user";
+
+export interface LoginDatasourceInf {
+ getToken(form: LoginFormValues): Promise;
+}
diff --git a/web/src/infraestructure/entities/login_form_values.ts b/web/src/infraestructure/entities/login_form_values.ts
new file mode 100644
index 0000000000000000000000000000000000000000..78456b1e9ed6c071fce5c8e5ffcca45d45472772
--- /dev/null
+++ b/web/src/infraestructure/entities/login_form_values.ts
@@ -0,0 +1,4 @@
+export interface LoginFormValues {
+ email : string
+ password : string
+};
\ No newline at end of file
diff --git a/web/src/infraestructure/entities/user.ts b/web/src/infraestructure/entities/user.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a600d77e58cefc74ca065fc6513610a2b847e47f
--- /dev/null
+++ b/web/src/infraestructure/entities/user.ts
@@ -0,0 +1,19 @@
+import { UserRole } from "../../constants/roles";
+
+export interface UserEntity {
+ name?: string;
+ lastName?: string;
+ email: string;
+ role: UserRole;
+}
+
+export interface LoggedInUser {
+ user: UserEntity;
+ token: string;
+}
+
+export interface UserInfo {
+ name?: string;
+ lastName?: string;
+ email: string;
+}
\ No newline at end of file
diff --git a/web/src/infraestructure/repositories/login_repository.ts b/web/src/infraestructure/repositories/login_repository.ts
new file mode 100644
index 0000000000000000000000000000000000000000..886c8eccdce3ccda29962845b14f68560cc78802
--- /dev/null
+++ b/web/src/infraestructure/repositories/login_repository.ts
@@ -0,0 +1,6 @@
+import { LoginFormValues } from "../entities/login_form_values";
+import { LoggedInUser } from "../entities/user";
+
+export interface LoginRepositoryInf{
+ getToken(form: LoginFormValues): Promise;
+}
\ No newline at end of file
diff --git a/web/src/pages/home/super_admin_page/assets/styles/style.css b/web/src/pages/home/super_admin_page/assets/styles/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..3ccfed0bd768c9376c8187c9dc0d7035396b9562
--- /dev/null
+++ b/web/src/pages/home/super_admin_page/assets/styles/style.css
@@ -0,0 +1,19 @@
+:root {
+ --shadow: 0px 2px 8px 0px gray;
+}
+
+
+.superdmin-panel-root{
+ display: flex;
+ height: 100%;
+ width: 100%;
+}
+
+
+.superadmin-panel-body{
+ height: 100vh;
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ background: #ECEAFF;
+}
diff --git a/web/src/pages/home/super_admin_page/super_admin_home_page.tsx b/web/src/pages/home/super_admin_page/super_admin_home_page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9c6d16fa5d76cebaa584699c42590c6d0c0c65db
--- /dev/null
+++ b/web/src/pages/home/super_admin_page/super_admin_home_page.tsx
@@ -0,0 +1,41 @@
+import { Menu, MenuItem, Sidebar } from "react-pro-sidebar"
+import './assets/styles/style.css';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faMonument, faSignOut, faUser, faUserTie } from "@fortawesome/free-solid-svg-icons";
+import { useState } from "react";
+import { useAuth } from "../../../context/auth_context";
+import { Link } from "react-router-dom";
+import { AdminPanelNavBar } from "../../../components/admin_panel_navbar/admin_navbar";
+import { SidebarHeader } from "../../../components/sidebar_header/sidebar_header";
+import { SuperadminPanelTownScreen } from "../../../components/sa_panel_town/sa_panel_town_screen/sa_panel_town_screen";
+import { SuperadminPanelAdminScreen } from "../../../components/sa_panel_admin/sa_panel_admin_screen/sa_panel_admin_screen";
+
+export const SuperAdminHomePage = () => {
+ const [collapsed, setCollapsed] = useState(true);
+ const [townPanel, setTownPanel] = useState(true);
+
+ return (
+
+
setCollapsed(false)}
+ onMouseOut={() => setCollapsed(true)}>
+
+
+
+
+
+
+ {townPanel ? : }
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/web/src/pages/login_page/login_page.tsx b/web/src/pages/login_page/login_page.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6cbc8ce23e8be06cd73e1c18f072edeef7a7b4ab
--- /dev/null
+++ b/web/src/pages/login_page/login_page.tsx
@@ -0,0 +1,60 @@
+import { usePasswoordVisibility } from "../../hooks/usePasswordVisibility"
+import "./styles/styles.css"
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faEye , faEyeSlash} from "@fortawesome/free-solid-svg-icons";
+import { useLogin } from "../../hooks/useLogin";
+
+export const LoginPage = () => {
+ const {
+ values,
+ handleClickShowPassword,
+ handleMouseDownPassword,
+ } = usePasswoordVisibility();
+
+ const {
+ register,
+ handleSubmit,
+ errors,
+ onSubmit
+ } = useLogin();
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/web/src/pages/login_page/styles/styles.css b/web/src/pages/login_page/styles/styles.css
new file mode 100644
index 0000000000000000000000000000000000000000..1d17cb075be9456b0ec4123e256e7057ca4e6116
--- /dev/null
+++ b/web/src/pages/login_page/styles/styles.css
@@ -0,0 +1,122 @@
+@import url('https://fonts.googleapis.com/css?family=Poppins:400,500,600,700&display=swap');
+
+* {
+ margin: 10;
+ padding: 0;
+ font-family: 'Poppins', sans-serif;
+ box-sizing: border-box;
+}
+
+.login-page-root{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ width: 100vw;
+}
+
+.login-form-container{
+ border-radius: 15px;
+ background: white;
+ display: grid;
+ place-items: center;
+ box-shadow: 0px 15px 20px rgba(0,0,0,0.1);
+}
+
+.login-form-container .title{
+ font-size: 35px;
+ font-weight: 600;
+ width: 100%;
+ color: white;
+ line-height: 50px;
+ text-align: center;
+ user-select: none;
+ border-radius: 15px 15px 0 0;
+ background: linear-gradient(-140deg,#413d3d, rgb(0, 0, 0))
+}
+
+.login-form-container form{
+ padding: 10px 40px 50px 30px;
+}
+
+.login-form-container .login-form-field{
+ position: relative;
+ height: 50px;
+ width: 100%;
+ margin-top: 20px;
+}
+
+.login-form-container form .login-form-field input{
+ height: 100%;
+ width: 100%;
+ outline: none;
+ font-size: 17px;
+ padding-left: 20px;
+ padding-right: 30px;
+ border: 1px solid lightgray;
+ border-radius: 15px;
+ transition: all 0.3s ease;
+}
+
+.login-form-container form .login-form-field input:focus,
+form .login-form-field input:valid{
+ border-color: black;
+}
+
+.login-form-container form .login-form-field label{
+ position: absolute;
+ top: 50%;
+ left: 20px;
+ color: #999999;
+ font-weight: 400;
+ font-size: 17px;
+ pointer-events: none;
+ transform: all 0.3s ease;
+ transform: translateY(-50%);
+}
+
+.login-form-container form .login-form-field .pass-visibility-button{
+ position: absolute;
+ top: 50%;
+ right: 5px;
+ transform: translateY(-50%);
+ transform: all 0.3s ease;
+ cursor: pointer;
+}
+
+
+form .login-form-field input:focus ~ label,
+form .login-form-field input:valid ~ label,
+form .login-form-field input[type="email"]:not(:placeholder-shown) ~ label,
+form .login-form-field input[type="email"]:focus ~ label {
+ top: 0%;
+ color: black;
+ font-size: 16px;
+ background-color: white;
+ transform: translateY(-50%);
+}
+
+.login-form-container form .login-form-button {
+ height: 50px;
+ width: 100%;
+ margin-top: 20px;
+ position: relative;
+}
+
+form .login-form-button input{
+ color: white;
+ border: none;
+ padding-left: 0;
+ margin-top: -10px;
+ font-size: 20px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: black;
+ width: 100%;
+ border-radius: 5px;
+}
+
+form .login-form-button input:active {
+ transform: scale(0.95);
+}
\ No newline at end of file
diff --git a/web/src/router/login_route.tsx b/web/src/router/login_route.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b401a7fb41f3f76da56fbcd4757275130fa23e10
--- /dev/null
+++ b/web/src/router/login_route.tsx
@@ -0,0 +1,10 @@
+import { Navigate, Outlet } from "react-router-dom";
+import { useAuth } from "../context/auth_context";
+
+export const LoginRoute = () => {
+ const {user} = useAuth();
+ if(user){
+ return ;
+ }
+ return ;
+}
\ No newline at end of file
diff --git a/web/src/router/protected_route.tsx b/web/src/router/protected_route.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2a412f384bcbe251c3bdef958253be25d3318984
--- /dev/null
+++ b/web/src/router/protected_route.tsx
@@ -0,0 +1,15 @@
+import { Navigate, Outlet } from "react-router-dom";
+import { useAuth } from "../context/auth_context";
+
+interface ProtectedRouteProps {
+ allowedRoles?: string[];
+}
+
+export const ProtectedRoute = ({allowedRoles}: ProtectedRouteProps) => {
+ const {user} = useAuth();
+ if(!user){
+ return ;
+ }
+
+ return ;
+}
\ No newline at end of file
diff --git a/web/src/router/router.tsx b/web/src/router/router.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..9e051b3af36d2dadde7c7e533ad005fa60446b03
--- /dev/null
+++ b/web/src/router/router.tsx
@@ -0,0 +1,27 @@
+import { createBrowserRouter } from "react-router-dom";
+import { SUPERADMIN_ROLES, ADMIN_ROLES } from "../constants/roles";
+import { LoginPage } from "../pages/login_page/login_page";
+import { ProtectedRoute } from "./protected_route";
+import { SuperAdminHomePage } from "../pages/home/super_admin_page/super_admin_home_page";
+
+export const router = createBrowserRouter([
+ {
+ element: ,
+ children: [
+ {
+ index: true,
+ path: "/",
+ element:
+ }
+ ]
+ },
+ {
+ element: ,
+ children: [
+ {
+ path: "/login",
+ element:
+ }
+ ]
+ }
+]);
\ No newline at end of file