From 7835b852c5de5b028e6d0f4afffa1d4e24782cf1 Mon Sep 17 00:00:00 2001 From: Aryaman Kumar Sharma Date: Thu, 30 Jan 2025 01:41:19 +0530 Subject: [PATCH 01/40] Fix: Ensure profile panel text and button visibility in dark mode --- Client/src/Components/Inputs/Image/index.jsx | 2 +- Client/src/Components/TabPanels/Account/ProfilePanel.jsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/Inputs/Image/index.jsx b/Client/src/Components/Inputs/Image/index.jsx index a955a8ad6..46bcfe694 100644 --- a/Client/src/Components/Inputs/Image/index.jsx +++ b/Client/src/Components/Inputs/Image/index.jsx @@ -99,7 +99,7 @@ const ImageField = ({ id, src, loading, onChange }) => { Click to upload diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index e20cb13a4..7b94c5b16 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -445,7 +445,7 @@ const ProfilePanel = () => { > From 959e555744f6390a832a65cfb279bd227697641c Mon Sep 17 00:00:00 2001 From: Aryaman Kumar Sharma Date: Thu, 30 Jan 2025 17:44:28 +0530 Subject: [PATCH 02/40] Fix: Enhance MuiButton disabled state styles in theme --- Client/src/Components/Inputs/Image/index.jsx | 2 +- Client/src/Components/TabPanels/Account/ProfilePanel.jsx | 8 +------- Client/src/Utils/Theme/globalTheme.js | 4 ++++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Client/src/Components/Inputs/Image/index.jsx b/Client/src/Components/Inputs/Image/index.jsx index 46bcfe694..6a16b9300 100644 --- a/Client/src/Components/Inputs/Image/index.jsx +++ b/Client/src/Components/Inputs/Image/index.jsx @@ -99,7 +99,7 @@ const ImageField = ({ id, src, loading, onChange }) => { Click to upload diff --git a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx index 7b94c5b16..e20cb13a4 100644 --- a/Client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/Client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -445,7 +445,7 @@ const ProfilePanel = () => { > diff --git a/Client/src/Utils/Theme/globalTheme.js b/Client/src/Utils/Theme/globalTheme.js index 12c377b3e..8805fe69f 100644 --- a/Client/src/Utils/Theme/globalTheme.js +++ b/Client/src/Utils/Theme/globalTheme.js @@ -110,6 +110,10 @@ const baseTheme = (palette) => ({ "&:hover": { boxShadow: "none", }, + "&.Mui-disabled": { + backgroundColor: theme.palette.secondary.main, + color: theme.palette.primary.main, + }, "&.MuiLoadingButton-root": { "&:disabled": { backgroundColor: theme.palette.secondary.main, From 263427dab3ec2fb59f5deaadd5371989590d4c68 Mon Sep 17 00:00:00 2001 From: Aryaman Kumar Sharma Date: Thu, 6 Feb 2025 10:48:14 +0530 Subject: [PATCH 03/40] Fix: Disabled button color theme --- Client/src/Utils/Theme/globalTheme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Utils/Theme/globalTheme.js b/Client/src/Utils/Theme/globalTheme.js index 7f90705d0..bfa26a0e6 100644 --- a/Client/src/Utils/Theme/globalTheme.js +++ b/Client/src/Utils/Theme/globalTheme.js @@ -130,7 +130,7 @@ const baseTheme = (palette) => ({ }, "&.Mui-disabled": { backgroundColor: theme.palette.secondary.main, - color: theme.palette.primary.main, + color: theme.palette.primary.contrastText, }, "&.MuiLoadingButton-root": { "&:disabled": { From 46ef6b01a2e5f12baa8624f96e8acdf500f0eae9 Mon Sep 17 00:00:00 2001 From: cihatata Date: Fri, 7 Feb 2025 01:59:54 +0300 Subject: [PATCH 04/40] feat: i18n support for client --- Client/package-lock.json | 273 ++++++++++++++++++ Client/package.json | 3 + Client/src/Components/LanguageSelector.jsx | 139 +++++++++ .../Pages/Auth/Login/Components/EmailStep.jsx | 10 +- .../Login/Components/ForgotPasswordLabel.jsx | 10 +- .../Auth/Login/Components/PasswordStep.jsx | 13 +- Client/src/Pages/Auth/Login/Login.jsx | 23 +- Client/src/Pages/Auth/Register/Register.jsx | 21 +- .../Pages/Auth/Register/StepThree/index.jsx | 43 +-- .../src/Pages/Auth/Register/StepTwo/index.jsx | 12 +- Client/src/Pages/Auth/SetNewPassword.jsx | 44 +-- Client/src/Utils/NetworkService.js | 16 + Client/src/Utils/i18n.js | 36 +++ Client/src/locales/gb.json | 59 ++++ Client/src/locales/tr.json | 59 ++++ Client/src/main.jsx | 3 +- 16 files changed, 688 insertions(+), 76 deletions(-) create mode 100644 Client/src/Components/LanguageSelector.jsx create mode 100644 Client/src/Utils/i18n.js create mode 100644 Client/src/locales/gb.json create mode 100644 Client/src/locales/tr.json diff --git a/Client/package-lock.json b/Client/package-lock.json index 51a0c8964..c4d981e54 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -22,6 +22,7 @@ "axios": "^1.7.4", "dayjs": "1.11.13", "flag-icons": "7.3.2", + "i18next": "^24.2.2", "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", @@ -31,10 +32,12 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-i18next": "^15.4.0", "react-redux": "9.2.0", "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", + "react-world-flags": "^1.6.0", "recharts": "2.15.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" @@ -2364,6 +2367,14 @@ "@svgr/core": "*" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2868,6 +2879,11 @@ "dev": true, "license": "MIT" }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3060,6 +3076,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3112,6 +3136,74 @@ "tiny-invariant": "^1.0.6" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==" + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -3436,6 +3528,57 @@ "csstype": "^3.0.2" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dot-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", @@ -4493,6 +4636,44 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "24.2.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz", + "integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5292,6 +5473,11 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, "node_modules/memoize-one": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", @@ -5413,6 +5599,17 @@ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", "license": "MIT" }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5888,6 +6085,27 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.0.tgz", + "integrity": "sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -6003,6 +6221,19 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-world-flags": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/react-world-flags/-/react-world-flags-1.6.0.tgz", + "integrity": "sha512-eutSeAy5YKoVh14js/JUCSlA6EBk1n4k+bDaV+NkNB50VhnG+f4QDTpYycnTUTsZ5cqw/saPmk0Z4Fa0VVZ1Iw==", + "dependencies": { + "svg-country-flags": "^1.2.10", + "svgo": "^3.0.2", + "world-countries": "^5.0.0" + }, + "peerDependencies": { + "react": ">=0.14" + } + }, "node_modules/recharts": { "version": "2.15.1", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", @@ -6630,12 +6861,41 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-country-flags": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/svg-country-flags/-/svg-country-flags-1.2.10.tgz", + "integrity": "sha512-xrqwo0TYf/h2cfPvGpjdSuSguUbri4vNNizBnwzoZnX0xGo3O5nGJMlbYEp7NOYcnPGBm6LE2axqDWSB847bLw==" + }, "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "license": "MIT" }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6936,6 +7196,14 @@ "vite": ">=2.6.0" } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", @@ -7061,6 +7329,11 @@ "node": ">=0.10.0" } }, + "node_modules/world-countries": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/world-countries/-/world-countries-5.0.0.tgz", + "integrity": "sha512-wAfOT9Y5i/xnxNOdKJKXdOCw9Q3yQLahBUeuRol+s+o20F6h2a4tLEbJ1lBCYwEQ30Sf9Meqeipk1gib3YwF5w==" + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/Client/package.json b/Client/package.json index 6b006e24e..2d0882be9 100644 --- a/Client/package.json +++ b/Client/package.json @@ -25,6 +25,7 @@ "axios": "^1.7.4", "dayjs": "1.11.13", "flag-icons": "7.3.2", + "i18next": "^24.2.2", "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", @@ -34,10 +35,12 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-i18next": "^15.4.0", "react-redux": "9.2.0", "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", + "react-world-flags": "^1.6.0", "recharts": "2.15.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" diff --git a/Client/src/Components/LanguageSelector.jsx b/Client/src/Components/LanguageSelector.jsx new file mode 100644 index 000000000..9c762e534 --- /dev/null +++ b/Client/src/Components/LanguageSelector.jsx @@ -0,0 +1,139 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Box, MenuItem, Select, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import Flag from "react-world-flags"; + +// Dil kodlarını ülke kodlarına eşleştirme +const COUNTRY_CODES = { + en: "GB", + tr: "TR", +}; + +const LanguageSelector = () => { + const { i18n } = useTranslation(); + const theme = useTheme(); + const [language, setLanguage] = useState(i18n.language || "en"); + + const handleChange = (event) => { + const newLang = event.target.value; + setLanguage(newLang); + i18n.changeLanguage(newLang); + }; + + // i18n instance'ından mevcut dilleri al + const languages = Object.keys(i18n.options.resources || {}); + + return ( + + ); +}; + +export default LanguageSelector; diff --git a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx index b54f46d12..ec5a4b2db 100644 --- a/Client/src/Pages/Auth/Login/Components/EmailStep.jsx +++ b/Client/src/Pages/Auth/Login/Components/EmailStep.jsx @@ -3,6 +3,7 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import TextInput from "../../../../Components/Inputs/TextInput"; import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; /** * Renders the email step of the login process which includes an email field. @@ -18,6 +19,7 @@ import PropTypes from "prop-types"; const EmailStep = ({ form, errors, onSubmit, onChange }) => { const theme = useTheme(); const inputRef = useRef(null); + const { t } = useTranslation(); useEffect(() => { if (inputRef.current) { @@ -33,8 +35,8 @@ const EmailStep = ({ form, errors, onSubmit, onChange }) => { position="relative" > - Log In - Enter your email address + {t("authLoginTitle")} + {t("authLoginEnterEmail")} { { }, }} > - Continue + {t("continue")} diff --git a/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx index d7a80e5f5..a9cb17d7c 100644 --- a/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx +++ b/Client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx @@ -1,10 +1,12 @@ import { Box, Typography, useTheme } from "@mui/material"; import PropTypes from "prop-types"; import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; const ForgotPasswordLabel = ({ email, errorEmail }) => { const theme = useTheme(); const navigate = useNavigate(); + const { t } = useTranslation(); const handleNavigate = () => { if (email !== "" && !errorEmail) { @@ -20,7 +22,7 @@ const ForgotPasswordLabel = ({ email, errorEmail }) => { display="inline-block" color={theme.palette.primary.main} > - Forgot password? + {t("authForgotPasswordTitle")} { sx={{ userSelect: "none" }} onClick={handleNavigate} > - Reset password + {t("authForgotPasswordResetPassword")} ); }; -ForgotPasswordLabel.proptype = { +ForgotPasswordLabel.propTypes = { email: PropTypes.string.isRequired, - emailError: PropTypes.string.isRequired, + errorEmail: PropTypes.string.isRequired, }; export default ForgotPasswordLabel; diff --git a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx index 10ded02f5..298a1d105 100644 --- a/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx +++ b/Client/src/Pages/Auth/Login/Components/PasswordStep.jsx @@ -7,7 +7,7 @@ import TextInput from "../../../../Components/Inputs/TextInput"; import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import PropTypes from "prop-types"; - +import { useTranslation } from "react-i18next"; /** * Renders the password step of the login process, including a password input field. * @@ -23,6 +23,7 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => { const theme = useTheme(); const inputRef = useRef(null); const authState = useSelector((state) => state.auth); + const { t } = useTranslation(); useEffect(() => { if (inputRef.current) { @@ -38,8 +39,8 @@ const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => { textAlign="center" > - Log In - Enter your password + {t("authLoginTitle")} + {t("authLoginEnterPassword")} { { }} > - Back + {t("commonBack")}{" "} { }, }} > - Continue + {t("continue")} diff --git a/Client/src/Pages/Auth/Login/Login.jsx b/Client/src/Pages/Auth/Login/Login.jsx index 30743c60c..60aaed6ba 100644 --- a/Client/src/Pages/Auth/Login/Login.jsx +++ b/Client/src/Pages/Auth/Login/Login.jsx @@ -15,6 +15,7 @@ import EmailStep from "./Components/EmailStep"; import PasswordStep from "./Components/PasswordStep"; import ThemeSwitch from "../../../Components/ThemeSwitch"; import ForgotPasswordLabel from "./Components/ForgotPasswordLabel"; +import LanguageSelector from "../../../Components/LanguageSelector"; const DEMO = import.meta.env.VITE_APP_DEMO; @@ -163,11 +164,26 @@ const Login = () => { - - Checkmate + + + Checkmate + + + + + { email={form.email} errorEmail={errors.email} /> - - - ); diff --git a/Client/src/Pages/Auth/Register/Register.jsx b/Client/src/Pages/Auth/Register/Register.jsx index 9e8248cf8..9f93cc821 100644 --- a/Client/src/Pages/Auth/Register/Register.jsx +++ b/Client/src/Pages/Auth/Register/Register.jsx @@ -15,7 +15,7 @@ import Background from "../../../assets/Images/background-grid.svg?react"; import Logo from "../../../assets/icons/checkmate-icon.svg?react"; import Mail from "../../../assets/icons/mail.svg?react"; import "../index.css"; - +import { useTranslation } from "react-i18next"; /** * Displays the initial landing page. * @@ -26,7 +26,7 @@ import "../index.css"; */ const LandingPage = ({ isSuperAdmin, onSignup }) => { const theme = useTheme(); - + const { t } = useTranslation(); return ( <> { Sign Up - Create your {isSuperAdmin ? "Super admin " : ""}account to get started. + {isSuperAdmin + ? t("authRegisterCreateSuperAdminAccount") + : t("authRegisterCreateAccount")} @@ -60,12 +62,12 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => { }} > - Sign up with Email + {t("authRegisterSignUpWithEmail")} - By signing up, you agree to our{" "} + {t("authRegisterBySigningUp")} { @@ -118,6 +120,7 @@ const Register = ({ isSuperAdmin }) => { const navigate = useNavigate(); const { token } = useParams(); const theme = useTheme(); + const { t } = useTranslation(); // TODO If possible, change the IDs of these fields to match the backend const idMap = { "register-firstname-input": "firstName", @@ -307,7 +310,7 @@ const Register = ({ isSuperAdmin }) => { gap={theme.spacing(4)} > - Checkmate + {t("commonAppName")} { textAlign="center" p={theme.spacing(12)} > - Already have an account? — + + {t("authRegisterAlreadyHaveAccount")} - + { }} sx={{ userSelect: "none", color: theme.palette.primary.main }} > - Log In + {t("authLoginTitle")} diff --git a/Client/src/Pages/Auth/Register/StepThree/index.jsx b/Client/src/Pages/Auth/Register/StepThree/index.jsx index 6221a8ef1..eb05f843a 100644 --- a/Client/src/Pages/Auth/Register/StepThree/index.jsx +++ b/Client/src/Pages/Auth/Register/StepThree/index.jsx @@ -6,7 +6,7 @@ import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import TextInput from "../../../../Components/Inputs/TextInput"; import Check from "../../../../Components/Check/Check"; import { useValidatePassword } from "../../hooks/useValidatePassword"; - +import { useTranslation } from "react-i18next"; StepThree.propTypes = { onSubmit: PropTypes.func, onBack: PropTypes.func, @@ -23,6 +23,7 @@ StepThree.propTypes = { function StepThree({ onSubmit, onBack }) { const theme = useTheme(); const inputRef = useRef(null); + const { t } = useTranslation(); useEffect(() => { if (inputRef.current) { @@ -38,8 +39,8 @@ function StepThree({ onSubmit, onBack }) { textAlign="center" > - Sign Up - Create your password + {t("signUp")} + {t("createPassword")} @@ -140,7 +141,7 @@ function StepThree({ onSubmit, onBack }) { }} > - Back + {t("commonBack")} diff --git a/Client/src/Pages/Auth/Register/StepTwo/index.jsx b/Client/src/Pages/Auth/Register/StepTwo/index.jsx index da247cdf9..a16d5344b 100644 --- a/Client/src/Pages/Auth/Register/StepTwo/index.jsx +++ b/Client/src/Pages/Auth/Register/StepTwo/index.jsx @@ -4,6 +4,7 @@ import { useTheme } from "@emotion/react"; import { Box, Button, Stack, Typography } from "@mui/material"; import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; import TextInput from "../../../../Components/Inputs/TextInput"; +import { useTranslation } from "react-i18next"; StepTwo.propTypes = { form: PropTypes.object, @@ -27,6 +28,7 @@ StepTwo.propTypes = { function StepTwo({ form, errors, onSubmit, onChange, onBack }) { const theme = useTheme(); const inputRef = useRef(null); + const { t } = useTranslation(); useEffect(() => { if (inputRef.current) { @@ -41,8 +43,8 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) { textAlign="center" > - Sign Up - Enter your email address + {t("signUp")} + {t("enterEmail")} - Back + {t("commonBack")} diff --git a/Client/src/Pages/Auth/SetNewPassword.jsx b/Client/src/Pages/Auth/SetNewPassword.jsx index a4c3b0a5a..630ca23f0 100644 --- a/Client/src/Pages/Auth/SetNewPassword.jsx +++ b/Client/src/Pages/Auth/SetNewPassword.jsx @@ -17,11 +17,13 @@ import Logo from "../../assets/icons/checkmate-icon.svg?react"; import Background from "../../assets/Images/background-grid.svg?react"; import "./index.css"; import { useValidatePassword } from "./hooks/useValidatePassword"; +import { useTranslation } from "react-i18next"; const SetNewPassword = () => { const navigate = useNavigate(); const dispatch = useDispatch(); const theme = useTheme(); + const { t } = useTranslation(); const passwordId = useId(); const confirmPasswordId = useId(); @@ -97,7 +99,7 @@ const SetNewPassword = () => { gap={theme.spacing(4)} > - Checkmate + {t("commonAppName")} { - Set new password - - Your new password must be different to previously used passwords. - + {t("authSetNewPasswordTitle")} + {t("authSetNewPasswordDescription")} { id={passwordId} type="password" name="password" - label="Password" + label={t("commonPassword")} isRequired={true} placeholder="••••••••" value={form.password} @@ -186,7 +186,7 @@ const SetNewPassword = () => { id={confirmPasswordId} type="password" name="confirm" - label="Confirm password" + label={t("authSetNewPasswordConfirmPassword")} isRequired={true} placeholder="••••••••" value={form.confirm} @@ -201,33 +201,33 @@ const SetNewPassword = () => { mb={theme.spacing(12)} > @@ -244,7 +244,7 @@ const SetNewPassword = () => { } sx={{ width: "100%", maxWidth: 400 }} > - Reset password + {t("authSetNewPasswordResetPassword")} @@ -252,7 +252,7 @@ const SetNewPassword = () => { textAlign="center" p={theme.spacing(12)} > - Go back to — + {t("goBackTo")} — { onClick={() => navigate("/login")} sx={{ userSelect: "none" }} > - Log In + {t("authLoginTitle")} diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 254138ff3..cc76ede2e 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -1,4 +1,5 @@ import axios from "axios"; +import i18next from 'i18next'; const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; const FALLBACK_BASE_URL = "http://localhost:5000/api/v1"; import { clearAuthState } from "../Features/Auth/authSlice"; @@ -23,6 +24,21 @@ class NetworkService { } this.setBaseUrl(baseURL); }); + this.axiosInstance.interceptors.request.use( + (config) => { + const currentLanguage = i18next.language || 'en'; + + config.headers = { + ...config.headers, + "Accept-Language": currentLanguage, + }; + + return config; + }, + (error) => { + return Promise.reject(error); + } + ); this.axiosInstance.interceptors.response.use( (response) => response, (error) => { diff --git a/Client/src/Utils/i18n.js b/Client/src/Utils/i18n.js new file mode 100644 index 000000000..076a8b664 --- /dev/null +++ b/Client/src/Utils/i18n.js @@ -0,0 +1,36 @@ +import i18n from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +const primaryLanguage = 'gb'; + +const translations = import.meta.glob('../locales/*.json', { eager: true }); + +const resources = {}; +Object.keys(translations).forEach((path) => { + const langCode = path.match(/\/([^/]+)\.json$/)[1]; + resources[langCode] = { + translation: translations[path].default || translations[path] + }; +}); + +const savedLanguage = localStorage.getItem("language") || primaryLanguage; + +i18n + .use(initReactI18next) + .init({ + resources, + lng: savedLanguage, + fallbackLng: primaryLanguage, + debug: import.meta.env.MODE === 'development', + ns: ['translation'], + defaultNS: 'translation', + interpolation: { + escapeValue: false, + }, + }); + +i18n.on("languageChanged", (lng) => { + localStorage.setItem("language", lng); +}); + +export default i18n; diff --git a/Client/src/locales/gb.json b/Client/src/locales/gb.json new file mode 100644 index 000000000..03dd02c35 --- /dev/null +++ b/Client/src/locales/gb.json @@ -0,0 +1,59 @@ +{ + "dontHaveAccount": "Don't have account", + "email": "E-mail", + "forgotPassword": "Forgot Password", + "password": "password", + "signUp": "Sign up", + "submit": "Submit", + "title": "Title", + "continue": "Continue", + "enterEmail": "Enter your email", + "authLoginTitle": "Log In", + "authLoginEnterPassword": "Enter your password", + "commonPassword": "Password", + "createPassword": "Create your password", + "createAPassword": "Create a password", + "commonBack": "Back", + "authForgotPasswordTitle": "Forgot password?", + "authForgotPasswordResetPassword": "Reset password", + "authRegisterAlreadyHaveAccount": "Already have an account?", + "commonAppName": "Checkmate", + "authLoginEnterEmail": "Enter your email", + "authRegisterTitle": "Create an account", + "authRegisterStepOneTitle": "Create your account", + "authRegisterStepOneDescription": "Enter your details to get started", + "authRegisterStepTwoTitle": "Set up your profile", + "authRegisterStepTwoDescription": "Tell us more about yourself", + "authRegisterStepThreeTitle": "Almost done!", + "authRegisterStepThreeDescription": "Review your information", + "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", + "authForgotPasswordSendInstructions": "Send instructions", + "authForgotPasswordBackTo": "Back to", + "authCheckEmailTitle": "Check your email", + "authCheckEmailDescription": "We sent a password reset link to {{email}}", + "authCheckEmailResendEmail": "Resend email", + "authCheckEmailBackTo": "Back to", + "goBackTo": "Go back to", + "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", + "authCheckEmailClickToResend": "Click to resend", + "authSetNewPasswordTitle": "Set new password", + "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", + "authSetNewPasswordNewPassword": "New password", + "authSetNewPasswordConfirmPassword": "Confirm password", + "confirmPassword": "Confirm your password", + "authSetNewPasswordResetPassword": "Reset password", + "authSetNewPasswordBackTo": "Back to", + "authPasswordMustBeAtLeast": "Must be at least", + "authPasswordCharactersLong": "8 characters long", + "authPasswordMustContainAtLeast": "Must contain at least", + "authPasswordSpecialCharacter": "one special character", + "authPasswordOneNumber": "one number", + "authPasswordUpperCharacter": "one upper character", + "authPasswordLowerCharacter": "one lower character", + "authPasswordConfirmAndPassword": "Confirm password and password", + "authPasswordMustMatch": "must match", + "authRegisterCreateAccount": "Create your account to get started", + "authRegisterCreateSuperAdminAccount": "Create your Super admin account to get started", + "authRegisterSignUpWithEmail": "Sign up with Email", + "authRegisterBySigningUp": "By signing up, you agree to our" +} \ No newline at end of file diff --git a/Client/src/locales/tr.json b/Client/src/locales/tr.json new file mode 100644 index 000000000..1b42f634b --- /dev/null +++ b/Client/src/locales/tr.json @@ -0,0 +1,59 @@ +{ + "dontHaveAccount": "Hesabınız yok mu", + "email": "E-posta", + "forgotPassword": "Parolamı unuttum", + "password": "Parola", + "signUp": "Kayıt Ol", + "submit": "Gönder", + "title": "Başlık", + "continue": "Devam Et", + "enterEmail": "E-posta adresinizi girin", + "authLoginTitle": "Giriş Yap", + "authLoginEnterPassword": "Parolanızı girin", + "commonPassword": "Parola", + "createPassword": "Parolanızı oluşturun", + "createAPassword": "Bir parola oluşturun", + "commonBack": "Geri", + "authForgotPasswordTitle": "Parolanızı mı unuttunuz?", + "authForgotPasswordResetPassword": "Parola sıfırla", + "authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?", + "commonAppName": "Checkmate", + "authLoginEnterEmail": "E-posta adresinizi girin", + "authRegisterTitle": "Hesap oluştur", + "authRegisterStepOneTitle": "Hesabınızı oluşturun", + "authRegisterStepOneDescription": "Başlamak için bilgilerinizi girin", + "authRegisterStepTwoTitle": "Profilinizi ayarlayın", + "authRegisterStepTwoDescription": "Kendiniz hakkında daha fazla bilgi verin", + "authRegisterStepThreeTitle": "Neredeyse bitti!", + "authRegisterStepThreeDescription": "Bilgilerinizi gözden geçirin", + "authForgotPasswordDescription": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.", + "authForgotPasswordSendInstructions": "Talimatları gönder", + "authForgotPasswordBackTo": "Geri dön", + "authCheckEmailTitle": "E-postanızı kontrol edin", + "authCheckEmailDescription": "{{email}} adresine şifre sıfırlama bağlantısı gönderdik", + "authCheckEmailResendEmail": "E-postayı yeniden gönder", + "authCheckEmailBackTo": "Geri dön", + "goBackTo": "Geri dön", + "authCheckEmailDidntReceiveEmail": "E-posta almadınız mı?", + "authCheckEmailClickToResend": "Yeniden göndermek için tıklayın", + "authSetNewPasswordTitle": "Yeni şifre belirle", + "authSetNewPasswordDescription": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.", + "authSetNewPasswordNewPassword": "Yeni şifre", + "authSetNewPasswordConfirmPassword": "Parolayı onayla", + "confirmPassword": "Parolanızı onaylayın", + "authSetNewPasswordResetPassword": "Parola sıfırla", + "authSetNewPasswordBackTo": "Geri dön", + "authPasswordMustBeAtLeast": "En az", + "authPasswordCharactersLong": "8 karakter uzunluğunda olmalı", + "authPasswordMustContainAtLeast": "En az içermeli", + "authPasswordSpecialCharacter": "bir özel karakter", + "authPasswordOneNumber": "bir rakam", + "authPasswordUpperCharacter": "bir büyük harf", + "authPasswordLowerCharacter": "bir küçük harf", + "authPasswordConfirmAndPassword": "Onay şifresi ve şifre", + "authPasswordMustMatch": "eşleşmelidir", + "authRegisterCreateAccount": "Hesap oluşturmak için devam et", + "authRegisterCreateSuperAdminAccount": "Super admin hesabınızı oluşturmak için devam edin", + "authRegisterSignUpWithEmail": "E-posta ile kayıt ol", + "authRegisterBySigningUp": "Kayıt olarak, aşağıdaki şartları kabul ediyorsunuz:" +} \ No newline at end of file diff --git a/Client/src/main.jsx b/Client/src/main.jsx index 7ee52132e..4be45e6d7 100644 --- a/Client/src/main.jsx +++ b/Client/src/main.jsx @@ -1,6 +1,7 @@ import ReactDOM from "react-dom/client"; import App from "./App.jsx"; import "./index.css"; +import "./Utils/i18n"; import { BrowserRouter as Router } from "react-router-dom"; import { Provider } from "react-redux"; import { persistor, store } from "./store"; @@ -17,7 +18,7 @@ ReactDOM.createRoot(document.getElementById("root")).render( > - + From d1ac398deb41ed24a40b822233c47d36be8958ad Mon Sep 17 00:00:00 2001 From: cihatata Date: Fri, 7 Feb 2025 02:03:25 +0300 Subject: [PATCH 05/40] feat: flag margin --- Client/src/Components/LanguageSelector.jsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Client/src/Components/LanguageSelector.jsx b/Client/src/Components/LanguageSelector.jsx index 9c762e534..a58093670 100644 --- a/Client/src/Components/LanguageSelector.jsx +++ b/Client/src/Components/LanguageSelector.jsx @@ -13,7 +13,7 @@ const COUNTRY_CODES = { const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); - const [language, setLanguage] = useState(i18n.language || "en"); + const [language, setLanguage] = useState(i18n.language || "gb"); const handleChange = (event) => { const newLang = event.target.value; @@ -54,7 +54,7 @@ const LanguageSelector = () => { top: "calc(50% - 8px)", }, "& .MuiSelect-select": { - padding: "2px 20px 2px 6px", + padding: "2px 20px 2px 8px", display: "flex", alignItems: "center", fontSize: 10, @@ -68,7 +68,7 @@ const LanguageSelector = () => { marginTop: 1, width: 64, "& .MuiMenuItem-root": { - padding: "2px 6px", + padding: "2px 8px", minHeight: 28, fontSize: 10, }, @@ -105,6 +105,7 @@ const LanguageSelector = () => { direction="row" spacing={2} alignItems="center" + ml={0.5} > { }, }} > - + Date: Fri, 7 Feb 2025 02:09:18 +0300 Subject: [PATCH 06/40] feat: flag margin --- Client/src/Components/LanguageSelector.jsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Client/src/Components/LanguageSelector.jsx b/Client/src/Components/LanguageSelector.jsx index a58093670..4f1193cd0 100644 --- a/Client/src/Components/LanguageSelector.jsx +++ b/Client/src/Components/LanguageSelector.jsx @@ -4,12 +4,6 @@ import { Box, MenuItem, Select, Stack } from "@mui/material"; import { useTheme } from "@emotion/react"; import Flag from "react-world-flags"; -// Dil kodlarını ülke kodlarına eşleştirme -const COUNTRY_CODES = { - en: "GB", - tr: "TR", -}; - const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); From f198e3c0729663c7ce5a2611d442b586ad3b1c84 Mon Sep 17 00:00:00 2001 From: Mend Renovate Date: Fri, 7 Feb 2025 21:31:48 +0000 Subject: [PATCH 07/40] Update material-ui monorepo to v7.26.0 --- Client/package-lock.json | 37 +++++++++++++++++++------------------ Client/package.json | 4 ++-- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Client/package-lock.json b/Client/package-lock.json index 51a0c8964..94ab57f92 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -16,8 +16,8 @@ "@mui/lab": "6.0.0-beta.26", "@mui/material": "6.4.3", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.25.0", - "@mui/x-date-pickers": "7.25.0", + "@mui/x-data-grid": "7.26.0", + "@mui/x-date-pickers": "7.26.0", "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", @@ -1500,15 +1500,15 @@ "license": "MIT" }, "node_modules/@mui/x-charts": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.25.0.tgz", - "integrity": "sha512-+DhnojHrVTt8RsTgq8AztzdFpW1kzOgiBdo0Pkl0DyxVdaKELC5QaetFwim9nIxT2zmU/RmiBcOU+qbqmQpFNA==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.26.0.tgz", + "integrity": "sha512-fla1pbMuyHhbWeDo6j5Vz8FHULdPnqACqQrXeLXo9p5kuJpcT9m28DQ1E3YmQ6xGD9NuaxiZdOaITT9PA2zMFQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", "@mui/x-charts-vendor": "7.20.0", - "@mui/x-internals": "7.25.0", + "@mui/x-internals": "7.26.0", "@react-spring/rafz": "^9.7.5", "@react-spring/web": "^9.7.5", "clsx": "^2.1.1", @@ -1558,17 +1558,18 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.25.0.tgz", - "integrity": "sha512-e9ZLbCgnDiADFiDyXo91ucZFHEMkKBNpwpkaTq5KohzefJfMpMQjTEbJeueSfBG2G1Q1Am2TPeBqrNeReIA7RQ==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.26.0.tgz", + "integrity": "sha512-9RNQeT2OL6jBOCE0MSUH11ol3fV5Zs9MkGxUIAGXcy/Fui0rZRNFO1yLmWDZU5yvskiNmUZJHWV/qXh++ZFarA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", - "@mui/x-internals": "7.25.0", + "@mui/x-internals": "7.26.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "reselect": "^5.1.1" + "reselect": "^5.1.1", + "use-sync-external-store": "^1.0.0" }, "engines": { "node": ">=14.0.0" @@ -1595,14 +1596,14 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.25.0.tgz", - "integrity": "sha512-t62OSFAKwj7KYQ8KcwTuKj6OgDuLQPSe4QUJcKDzD9rEhRIJVRUw2x27gBSdcls4l0PTrba19TghvDxCZprriw==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.26.0.tgz", + "integrity": "sha512-bhSDce1b5MBYYlCdHQJBThe10LGTE3D/u53TDQ41+IRj+iiNCun8jivw3DxKhmoBxlB+hVdkcltpTtIGlPjQZQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", - "@mui/x-internals": "7.25.0", + "@mui/x-internals": "7.26.0", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1661,9 +1662,9 @@ } }, "node_modules/@mui/x-internals": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.25.0.tgz", - "integrity": "sha512-tBUN54YznAkmtCIRAOl35Kgl0MjFDIjUbzIrbWRgVSIR3QJ8bXnVSkiRBi+P91SZEl9+ZW0rDj+osq7xFJV0kg==", + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz", + "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", diff --git a/Client/package.json b/Client/package.json index 6b006e24e..4a7de9ab8 100644 --- a/Client/package.json +++ b/Client/package.json @@ -19,8 +19,8 @@ "@mui/lab": "6.0.0-beta.26", "@mui/material": "6.4.3", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.25.0", - "@mui/x-date-pickers": "7.25.0", + "@mui/x-data-grid": "7.26.0", + "@mui/x-date-pickers": "7.26.0", "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", From 52ad36d75f3fbf4870990d076d9a2778245e8016 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:40:12 -0800 Subject: [PATCH 08/40] make fields optional --- Server/validation/joi.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Server/validation/joi.js b/Server/validation/joi.js index d595f00fd..349ac9fcb 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -421,8 +421,8 @@ const getStatusPageParamValidation = joi.object({ const createStatusPageBodyValidation = joi.object({ companyName: joi.string().required(), url: joi.string().required(), - timezone: joi.string().required(), - color: joi.string().required(), + timezone: joi.string().optional(), + color: joi.string().optional(), monitors: joi .array() .items(joi.string().pattern(/^[0-9a-fA-F]{24}$/)) @@ -434,7 +434,7 @@ const createStatusPageBodyValidation = joi.object({ "any.required": "Monitors are required", }), isPublished: joi.boolean(), - showCharts: joi.boolean(), + showCharts: joi.boolean().optional(), showUptimePercentage: joi.boolean(), }); From 3eaf68aa5bbd7784ac35d8b172d40d51317348ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:40:33 -0800 Subject: [PATCH 09/40] add src and name to image validation --- Client/src/Validation/validation.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index 988035cbe..6a2a56b94 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -180,6 +180,8 @@ const imageValidation = joi.object({ const logoImageValidation = joi .object({ + src: joi.string(), + name: joi.string(), type: joi .string() .valid("image/jpeg", "image/png") From b23ec3526077d86ee5f49fc03c40284cf6c2f789 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:41:06 -0800 Subject: [PATCH 10/40] add placeholder prop, add min width and height props --- Client/src/Components/Image/index.jsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Client/src/Components/Image/index.jsx b/Client/src/Components/Image/index.jsx index 9304a4385..8f30f8a9e 100644 --- a/Client/src/Components/Image/index.jsx +++ b/Client/src/Components/Image/index.jsx @@ -11,9 +11,12 @@ const Image = ({ alt, width = "auto", height = "auto", + minWidth = "auto", + minHeight = "auto", maxWidth = "auto", maxHeight = "auto", base64, + placeholder, sx, }) => { if (shouldRender === false) { @@ -28,11 +31,21 @@ const Image = ({ src = `data:image/png;base64,${base64}`; } + if ( + typeof src === "undefined" && + typeof base64 === "undefined" && + typeof placeholder !== "undefined" + ) { + src = placeholder; + } + return ( Date: Fri, 7 Feb 2025 14:42:04 -0800 Subject: [PATCH 11/40] add placeholder img --- Client/src/assets/Images/logo_placeholder.svg | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Client/src/assets/Images/logo_placeholder.svg diff --git a/Client/src/assets/Images/logo_placeholder.svg b/Client/src/assets/Images/logo_placeholder.svg new file mode 100644 index 000000000..82f307b2d --- /dev/null +++ b/Client/src/assets/Images/logo_placeholder.svg @@ -0,0 +1,5 @@ + + + + \ No newline at end of file From c015302811220f7dd7f855549c219f1f323d3117 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:42:24 -0800 Subject: [PATCH 12/40] update StatusPage model --- Server/db/models/StatusPage.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Server/db/models/StatusPage.js b/Server/db/models/StatusPage.js index e720327ed..2f9898300 100644 --- a/Server/db/models/StatusPage.js +++ b/Server/db/models/StatusPage.js @@ -15,12 +15,11 @@ const StatusPageSchema = mongoose.Schema( }, timezone: { type: String, - required: true, - default: "America/Toronto", + required: false, }, color: { type: String, - required: true, + required: false, default: "#4169E1", }, monitors: [ From 0cf26f82b77d6dd3268e7e9a2426361ca644cb85 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:42:38 -0800 Subject: [PATCH 13/40] add string --- Server/utils/messages.js | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/utils/messages.js b/Server/utils/messages.js index ffcfc3f1f..c8ff88f5a 100644 --- a/Server/utils/messages.js +++ b/Server/utils/messages.js @@ -135,6 +135,7 @@ const successMessages = { // Status Page STATUS_PAGE_BY_URL: "Got status page by url successfully", + STATUS_PAGE: "Got status page successfully", STATUS_PAGE_CREATE: "Status page created successfully", STATUS_PAGE_DELETE: "Status page deleted successfully", STATUS_PAGE_UPDATE: "Status page updated successfully", From b829bd45492ff8413a1d82ccc61f933b16263aeb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:43:14 -0800 Subject: [PATCH 14/40] add route for getting status page by url, update delete route to delete by url --- Server/routes/statusPageRoute.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/routes/statusPageRoute.js b/Server/routes/statusPageRoute.js index 37ae313ee..020935834 100644 --- a/Server/routes/statusPageRoute.js +++ b/Server/routes/statusPageRoute.js @@ -12,6 +12,7 @@ class StatusPageRoutes { initRoutes() { this.router.get("/", this.statusPageController.getStatusPage); + this.router.get("/:url", this.statusPageController.getStatusPageByUrl); this.router.post( "/", upload.single("logo"), @@ -24,7 +25,7 @@ class StatusPageRoutes { verifyJWT, this.statusPageController.updateStatusPage ); - this.router.delete("/", verifyJWT, this.statusPageController.deleteStatusPage); + this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage); } getRouter() { From 14ce47d97bba752357990584ea5e2ed602cb69aa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:44:08 -0800 Subject: [PATCH 15/40] add network request for getting status page by url, update create status page method --- Client/src/Utils/NetworkService.js | 38 ++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/Client/src/Utils/NetworkService.js b/Client/src/Utils/NetworkService.js index 52edfa91f..6d56990a9 100644 --- a/Client/src/Utils/NetworkService.js +++ b/Client/src/Utils/NetworkService.js @@ -974,19 +974,31 @@ class NetworkService { }); } + async getStatusPageByUrl(config) { + const { authToken, url } = config; + return this.axiosInstance.get(`/status-page/${url}`, { + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + }, + }); + } + async createStatusPage(config) { const { authToken, user, form, isCreate } = config; const fd = new FormData(); - fd.append("isPublished", form.isPublished); - fd.append("companyName", form.companyName); - fd.append("url", form.url); - fd.append("timezone", form.timezone); - fd.append("color", form.color); - fd.append("showCharts", form.showCharts); - fd.append("showUptimePercentage", form.showUptimePercentage); - form.monitors.forEach((monitorId) => { - fd.append("monitors[]", monitorId); - }); + form.isPublished && fd.append("isPublished", form.isPublished); + form.companyName && fd.append("companyName", form.companyName); + form.url && fd.append("url", form.url); + form.timezone && fd.append("timezone", form.timezone); + form.color && fd.append("color", form.color); + form.showCharts && fd.append("showCharts", form.showCharts); + form.showUptimePercentage && + fd.append("showUptimePercentage", form.showUptimePercentage); + form.monitors && + form.monitors.forEach((monitorId) => { + fd.append("monitors[]", monitorId); + }); if (form?.logo?.src && form?.logo?.src !== "") { const imageResult = await axios.get(form.logo.src, { responseType: "blob", @@ -1013,8 +1025,10 @@ class NetworkService { } async deleteStatusPage(config) { - const { authToken } = config; - return this.axiosInstance.delete(`/status-page`, { + const { authToken, url } = config; + const encodedUrl = encodeURIComponent(url); + + return this.axiosInstance.delete(`/status-page/${encodedUrl}`, { headers: { Authorization: `Bearer ${authToken}`, }, From 45cd6320d873dd9ba853d908160572ee8fc623ff Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:44:31 -0800 Subject: [PATCH 16/40] add routes --- Client/src/Routes/index.jsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx index 3ace64ae1..25a8b562d 100644 --- a/Client/src/Routes/index.jsx +++ b/Client/src/Routes/index.jsx @@ -31,6 +31,8 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details"; import DistributedUptimeMonitors from "../Pages/DistributedUptime/Monitors"; import CreateDistributedUptime from "../Pages/DistributedUptime/Create"; import DistributedUptimeDetails from "../Pages/DistributedUptime/Details"; +import DistributedUptimeStatus from "../Pages/DistributedUptime/Status"; +import CreateDistributedUptimeStatus from "../Pages/DistributedUptime/CreateStatus"; // Incidents import Incidents from "../Pages/Incidents"; @@ -110,6 +112,22 @@ const Routes = () => { } /> + + + + } + /> + + + + } + /> } From 27e4bd006c3e7ac408625bf154d9d305765f2b6b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:44:56 -0800 Subject: [PATCH 17/40] destructure monitors to add all properties --- Client/src/Hooks/useMonitorUtils.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Client/src/Hooks/useMonitorUtils.js b/Client/src/Hooks/useMonitorUtils.js index 1e235af68..fc1966b69 100644 --- a/Client/src/Hooks/useMonitorUtils.js +++ b/Client/src/Hooks/useMonitorUtils.js @@ -22,10 +22,7 @@ const useMonitorUtils = () => { } return { - id: monitor._id, - name: monitor.name, - url: monitor.url, - title: monitor.name, + ...monitor, percentage: uptimePercentage, percentageColor, monitor: monitor, From 65bc98ca2e9a6da17dd18ca63fa5e91054ec364c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:46:36 -0800 Subject: [PATCH 18/40] add db method for getting status page by url, update delete method to delete by url --- Server/db/mongo/modules/statusPageModule.js | 25 +++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Server/db/mongo/modules/statusPageModule.js b/Server/db/mongo/modules/statusPageModule.js index 2f31d2adb..7db9bb545 100644 --- a/Server/db/mongo/modules/statusPageModule.js +++ b/Server/db/mongo/modules/statusPageModule.js @@ -47,10 +47,21 @@ const updateStatusPage = async (statusPageData, image) => { } }; +const getStatusPageByUrl = async (url) => { + try { + const statusPage = await StatusPage.aggregate([{ $match: { url } }]); + return statusPage[0]; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getStatusPageByUrl"; + throw error; + } +}; + const getStatusPage = async () => { try { const statusPageQuery = await StatusPage.aggregate([ - { $limit: 1 }, + { $match: { url: "/status/public" } }, { $set: { originalMonitors: "$monitors", @@ -143,9 +154,9 @@ const getStatusPage = async () => { } }; -const deleteStatusPage = async () => { +const deleteStatusPage = async (url) => { try { - await StatusPage.deleteOne({}); + await StatusPage.deleteOne({ url }); } catch (error) { error.service = SERVICE_NAME; error.method = "createStatusPage"; @@ -153,4 +164,10 @@ const deleteStatusPage = async () => { } }; -export { createStatusPage, updateStatusPage, getStatusPage, deleteStatusPage }; +export { + createStatusPage, + updateStatusPage, + getStatusPage, + getStatusPageByUrl, + deleteStatusPage, +}; From c352ca39b1b19a66fe5babf5143b1bee999ac0de Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:47:22 -0800 Subject: [PATCH 19/40] add controller method for getting status page by url, update delete controlelr method --- Server/controllers/statusPageController.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Server/controllers/statusPageController.js b/Server/controllers/statusPageController.js index bcf241cc2..50f2e1ee4 100644 --- a/Server/controllers/statusPageController.js +++ b/Server/controllers/statusPageController.js @@ -62,7 +62,7 @@ class StatusPageController { try { const statusPage = await this.db.getStatusPage(); return res.success({ - msg: successMessages.STATUS_PAGE_BY_URL, + msg: successMessages.STATUS_PAGE, data: statusPage, }); } catch (error) { @@ -70,9 +70,21 @@ class StatusPageController { } }; + getStatusPageByUrl = async (req, res, next) => { + try { + const statusPage = await this.db.getStatusPageByUrl(req.params.url); + return res.success({ + msg: successMessages.STATUS_PAGE_BY_URL, + data: statusPage, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getStatusPageByUrl")); + } + }; + deleteStatusPage = async (req, res, next) => { try { - await this.db.deleteStatusPage(); + await this.db.deleteStatusPage(req.params.url); return res.success({ msg: successMessages.STATUS_PAGE_DELETE, }); From dec12cc6abef4a2804280a67bd2db712bde21bc7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:47:52 -0800 Subject: [PATCH 20/40] add label prop --- Client/src/Components/MonitorCreateHeader/index.jsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Client/src/Components/MonitorCreateHeader/index.jsx b/Client/src/Components/MonitorCreateHeader/index.jsx index 01a102b7d..565dca032 100644 --- a/Client/src/Components/MonitorCreateHeader/index.jsx +++ b/Client/src/Components/MonitorCreateHeader/index.jsx @@ -3,7 +3,12 @@ import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import SkeletonLayout from "./skeleton"; -const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => { +const CreateMonitorHeader = ({ + isAdmin, + label = "Create new", + shouldRender = true, + path, +}) => { const navigate = useNavigate(); if (!isAdmin) return null; if (!shouldRender) return ; @@ -18,7 +23,7 @@ const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => { color="accent" onClick={() => navigate(path)} > - Create new + {label} ); @@ -30,4 +35,5 @@ CreateMonitorHeader.propTypes = { isAdmin: PropTypes.bool.isRequired, shouldRender: PropTypes.bool, path: PropTypes.string.isRequired, + label: PropTypes.string, }; From d8c0839391ef9560043b759f83add6844461dcf1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:48:14 -0800 Subject: [PATCH 21/40] add early return --- .../Details/Hooks/useSubscribeToDetails.jsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx b/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx index 409e9cf89..787281865 100644 --- a/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx +++ b/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx @@ -59,6 +59,9 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { useEffect(() => { try { + if (typeof monitorId === "undefined") { + return; + } const cleanup = networkService.subscribeToDistributedUptimeDetails({ authToken, monitorId, @@ -116,4 +119,4 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger]; }; -export default useSubscribeToDetails; +export { useSubscribeToDetails }; From 163d1a3e6ed5d3a250285b5a2c0ab79b318ffc0b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:48:45 -0800 Subject: [PATCH 22/40] use correct _id instead of id --- .../Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 0b39e141b..10fffe5bd 100644 --- a/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -191,7 +191,7 @@ const UptimeDataTable = ({ }, }, onRowClick: (row) => { - navigate(`/uptime/${row.id}`); + navigate(`/uptime/${row._id}`); }, emptyView: "No monitors found", }} From 5c5d1b88e65f463611e0419799464ba36c9c37c8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:49:38 -0800 Subject: [PATCH 23/40] udpate max width, safe property access --- .../Pages/StatusPage/Status/Components/ControlsHeader/index.jsx | 2 +- .../Pages/StatusPage/Status/Components/MonitorsList/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx index 11733fc61..f07aa19da 100644 --- a/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx +++ b/Client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx @@ -80,7 +80,7 @@ const ControlsHeader = ({ statusPage, isDeleting, isDeleteOpen, setIsDeleteOpen {"Company {statusPage?.companyName} diff --git a/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx index ff7a18f97..7f4af1e0c 100644 --- a/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx +++ b/Client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx @@ -34,7 +34,7 @@ const MonitorsList = ({ monitors = [] }) => { gap={theme.spacing(20)} > - + Date: Fri, 7 Feb 2025 14:50:14 -0800 Subject: [PATCH 24/40] add url to request, add missing dependency --- .../Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx | 5 ++--- .../src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx index 42138d373..f9699020c 100644 --- a/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx +++ b/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx @@ -4,15 +4,14 @@ import { networkService } from "../../../../main"; import { createToast } from "../../../../Utils/toastUtils"; import { useNavigate } from "react-router-dom"; -const useStatusPageDelete = (fetchStatusPage) => { +const useStatusPageDelete = (fetchStatusPage, url = "/status/public") => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const { authToken } = useSelector((state) => state.auth); - const deleteStatusPage = async () => { try { setIsLoading(true); - await networkService.deleteStatusPage({ authToken }); + await networkService.deleteStatusPage({ authToken, url }); fetchStatusPage?.(); return true; } catch (error) { diff --git a/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx index d2b2eae2a..013e32dd9 100644 --- a/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx +++ b/Client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx @@ -18,11 +18,13 @@ const useStatusPageFetch = (isCreate = false) => { const response = await networkService.getStatusPage({ authToken }); if (!response?.data?.data) return; const { statusPage, monitors } = response.data.data; + setStatusPage(statusPage); const monitorsWithPercentage = monitors.map((monitor) => getMonitorWithPercentage(monitor, theme) ); + setMonitors(monitorsWithPercentage); } catch (error) { // If there is a 404, status page is not found @@ -35,7 +37,7 @@ const useStatusPageFetch = (isCreate = false) => { } finally { setIsLoading(false); } - }, [authToken, theme]); + }, [authToken, theme, getMonitorWithPercentage]); useEffect(() => { if (isCreate === true) { From 08b0ed9d3b4993c78e6ed15405f138fe6e029ba4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:51:02 -0800 Subject: [PATCH 25/40] remove comment --- Client/src/Pages/StatusPage/Status/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/Client/src/Pages/StatusPage/Status/index.jsx b/Client/src/Pages/StatusPage/Status/index.jsx index e27bc8184..a957f3f2c 100644 --- a/Client/src/Pages/StatusPage/Status/index.jsx +++ b/Client/src/Pages/StatusPage/Status/index.jsx @@ -139,7 +139,6 @@ const PublicStatus = () => { {link} { deleteStatusPage(); From 6a1675aa4ab5a889a4e0e767ec621f0acb17bd12 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 7 Feb 2025 14:51:29 -0800 Subject: [PATCH 26/40] implement DU status page --- .../Components/VisuallyHiddenInput/index.jsx | 22 ++ .../DistributedUptime/CreateStatus/index.jsx | 217 ++++++++++++++++++ .../Pages/DistributedUptime/Details/index.jsx | 11 +- .../Components/MonitorTable/index.jsx | 2 +- .../Status/Hooks/useStatusPageFetchByUrl.jsx | 35 +++ .../Pages/DistributedUptime/Status/index.jsx | 135 +++++++++++ .../Components/MonitorsTable/index.jsx | 2 +- 7 files changed, 420 insertions(+), 4 deletions(-) create mode 100644 Client/src/Pages/DistributedUptime/CreateStatus/Components/VisuallyHiddenInput/index.jsx create mode 100644 Client/src/Pages/DistributedUptime/CreateStatus/index.jsx create mode 100644 Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx create mode 100644 Client/src/Pages/DistributedUptime/Status/index.jsx diff --git a/Client/src/Pages/DistributedUptime/CreateStatus/Components/VisuallyHiddenInput/index.jsx b/Client/src/Pages/DistributedUptime/CreateStatus/Components/VisuallyHiddenInput/index.jsx new file mode 100644 index 000000000..fd002d2a4 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/CreateStatus/Components/VisuallyHiddenInput/index.jsx @@ -0,0 +1,22 @@ +const VisuallyHiddenInput = ({ onChange }) => { + return ( + + ); +}; + +export default VisuallyHiddenInput; diff --git a/Client/src/Pages/DistributedUptime/CreateStatus/index.jsx b/Client/src/Pages/DistributedUptime/CreateStatus/index.jsx new file mode 100644 index 000000000..d244bfe38 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/CreateStatus/index.jsx @@ -0,0 +1,217 @@ +// Components +import { Stack, Typography, Button, Box } from "@mui/material"; +import ConfigBox from "../../../Components/ConfigBox"; +import Checkbox from "../../../Components/Inputs/Checkbox"; +import TextInput from "../../../Components/Inputs/TextInput"; +import VisuallyHiddenInput from "./Components/VisuallyHiddenInput"; +import Image from "../../../Components/Image"; +import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +// Utils +import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage"; +import { useLocation } from "react-router-dom"; +import { statusPageValidation } from "../../../Validation/validation"; +import { buildErrors } from "../../../Validation/error"; +import { createToast } from "../../../Utils/toastUtils"; +import { useNavigate } from "react-router-dom"; + +const CreateStatus = () => { + const theme = useTheme(); + const { monitorId } = useParams(); + const navigate = useNavigate(); + const location = useLocation(); + const isCreate = location.pathname.startsWith("/distributed-uptime/status/create"); + const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate); + const BREADCRUMBS = [ + { name: "distributed uptime", path: "/distributed-uptime" }, + { name: "details", path: `/distributed-uptime/${monitorId}` }, + { name: "create status page", path: `` }, + ]; + // Local state + const [form, setForm] = useState({ + isPublished: false, + url: Math.floor(Math.random() * 1000000).toFixed(0), + logo: undefined, + companyName: "", + monitors: [monitorId], + }); + const [errors, setErrors] = useState({}); + + const handleFormChange = (e) => { + const { name, value, checked, type } = e.target; + + // Check for errors + const { error } = statusPageValidation.validate( + { [name]: value }, + { abortEarly: false } + ); + + setErrors((prev) => buildErrors(prev, name, error)); + + if (type === "checkbox") { + setForm({ ...form, [name]: checked }); + return; + } + setForm({ ...form, [name]: value }); + }; + + const handleImageUpload = (e) => { + const img = e.target?.files?.[0]; + setForm((prev) => ({ + ...prev, + logo: img, + })); + }; + const handleSubmit = async () => { + let logoToSubmit = undefined; + + // Handle image + if (typeof form.logo !== "undefined") { + logoToSubmit = { + src: URL.createObjectURL(form.logo), + name: form.logo.name, + type: form.logo.type, + size: form.logo.size, + }; + } + const formToSubmit = { ...form }; + if (typeof logoToSubmit !== "undefined") { + formToSubmit.logo = logoToSubmit; + } + + // Validate + const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false }); + if (typeof error === "undefined") { + const success = await createStatusPage({ form: formToSubmit }); + if (success) { + createToast({ body: "Status page created successfully" }); + navigate(`/distributed-uptime/status/${form.url}`); + } + return; + } + const newErrors = {}; + error?.details?.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + setErrors((prev) => ({ ...prev, ...newErrors })); + }; + + return ( + + + + + Create your{" "} + + + status page + + + + + Access + + If your status page is ready, you can mark it as published. + + + + + + + + + Basic Information + + Define company name and the subdomain that your status page points to. + + + + + + + + + + Logo + Upload a logo for your status page + + + Logo + + + + + + + + + + ); +}; + +export default CreateStatus; diff --git a/Client/src/Pages/DistributedUptime/Details/index.jsx b/Client/src/Pages/DistributedUptime/Details/index.jsx index ab39ec2ec..a8fc0c309 100644 --- a/Client/src/Pages/DistributedUptime/Details/index.jsx +++ b/Client/src/Pages/DistributedUptime/Details/index.jsx @@ -10,21 +10,23 @@ import StatBoxes from "./Components/StatBoxes"; import MonitorHeader from "./Components/MonitorHeader"; import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; import GenericFallback from "../../../Components/GenericFallback"; +import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; //Utils import { useTheme } from "@mui/material/styles"; import { useState } from "react"; import { useParams } from "react-router-dom"; -import useSubscribeToDetails from "./Hooks/useSubscribeToDetails"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useSubscribeToDetails } from "./Hooks/useSubscribeToDetails"; const DistributedUptimeDetails = () => { const { monitorId } = useParams(); - // Local State const [dateRange, setDateRange] = useState("day"); // Utils const theme = useTheme(); + const isAdmin = useIsAdmin(); const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] = useSubscribeToDetails({ monitorId, dateRange }); // Constants @@ -65,6 +67,11 @@ const DistributedUptimeDetails = () => { gap={theme.spacing(10)} > + { }, }, onRowClick: (row) => { - navigate(`/distributed-uptime/${row.id}`); + navigate(`/distributed-uptime/${row._id}`); }, }} /> diff --git a/Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx b/Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx new file mode 100644 index 000000000..973e157c7 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx @@ -0,0 +1,35 @@ +import { useState, useEffect } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useSelector } from "react-redux"; + +const useStatusPageFetchByUrl = ({ url }) => { + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [statusPage, setStatusPage] = useState(undefined); + const [monitorId, setMonitorId] = useState(undefined); + const { authToken } = useSelector((state) => state.auth); + useEffect(() => { + const fetchStatusPageByUrl = async () => { + try { + const response = await networkService.getStatusPageByUrl({ authToken, url }); + if (!response?.data?.data) return; + const statusPage = response.data.data; + setStatusPage(statusPage); + setMonitorId(statusPage?.monitors[0]); + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + fetchStatusPageByUrl(); + }, [authToken, url]); + + return [isLoading, networkError, statusPage, monitorId]; +}; + +export { useStatusPageFetchByUrl }; diff --git a/Client/src/Pages/DistributedUptime/Status/index.jsx b/Client/src/Pages/DistributedUptime/Status/index.jsx new file mode 100644 index 000000000..c2cee4c12 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Status/index.jsx @@ -0,0 +1,135 @@ +//Components +import DistributedUptimeMap from "../Details/Components/DistributedUptimeMap"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import { Stack, Typography } from "@mui/material"; +import DeviceTicker from "../Details/Components/DeviceTicker"; +import DistributedUptimeResponseChart from "../Details/Components/DistributedUptimeResponseChart"; +import NextExpectedCheck from "../Details/Components/NextExpectedCheck"; +import Footer from "../Details/Components/Footer"; +import StatBoxes from "../Details/Components/StatBoxes"; +import ControlsHeader from "../../StatusPage/Status/Components/ControlsHeader"; +import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; +import GenericFallback from "../../../Components/GenericFallback"; +import Dialog from "../../../Components/Dialog"; +//Utils +import { useTheme } from "@mui/material/styles"; +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useSubscribeToDetails } from "../Details/Hooks/useSubscribeToDetails"; +import { useStatusPageFetchByUrl } from "./Hooks/useStatusPageFetchByUrl"; +import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete"; +import { useNavigate } from "react-router-dom"; +const DistributedUptimeStatus = () => { + const { url } = useParams(); + // Local State + const [dateRange, setDateRange] = useState("day"); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + // Utils + const theme = useTheme(); + const navigate = useNavigate(); + const [statusPageIsLoading, statusPageNetworkError, statusPage, monitorId] = + useStatusPageFetchByUrl({ + url, + }); + + const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] = + useSubscribeToDetails({ monitorId, dateRange }); + + const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => { + navigate("/distributed-uptime"); + }, url); + // Constants + const BREADCRUMBS = [ + { name: "Distributed Uptime", path: "/distributed-uptime" }, + { name: "status", path: `` }, + ]; + + if (networkError) { + return ( + + + Network error + + Please check your connection + + ); + } + + if (typeof monitor === "undefined" || monitor.totalChecks === 0) { + return ( + + + + There is no check history for this monitor yet. + + + ); + } + + return ( + + + + + + + + + + + +