diff --git a/Client/package-lock.json b/Client/package-lock.json index 51a0c8964..556caf1bc 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -16,12 +16,13 @@ "@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", "flag-icons": "7.3.2", + "i18next": "^24.2.2", "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", @@ -31,6 +32,7 @@ "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", @@ -1500,15 +1502,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 +1560,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 +1598,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 +1664,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", @@ -3823,9 +3826,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.18.tgz", - "integrity": "sha512-IRGEoFn3OKalm3hjfolEWGqoF/jPqeEYFp+C8B0WMzwGwBMvlRDQd06kghDhF0C61uJ6WfSDhEZE/sAQjduKgw==", + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -4493,6 +4496,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", @@ -5740,9 +5781,9 @@ } }, "node_modules/prettier": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", - "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz", + "integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==", "dev": true, "license": "MIT", "bin": { @@ -5888,6 +5929,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", @@ -6936,6 +6998,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", diff --git a/Client/package.json b/Client/package.json index 6b006e24e..d4c26e1c5 100644 --- a/Client/package.json +++ b/Client/package.json @@ -19,12 +19,13 @@ "@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", "flag-icons": "7.3.2", + "i18next": "^24.2.2", "immutability-helper": "^3.1.1", "joi": "17.13.3", "jwt-decode": "^4.0.0", @@ -34,6 +35,7 @@ "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", diff --git a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx index 0b959a392..a1ead3cf9 100644 --- a/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/Client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx @@ -73,9 +73,12 @@ const CustomToolTip = ({ active, payload, label, dateRange }) => { component="span" sx={{ opacity: 0.8 }} > - Response Time - {" "} - + Response time: + + {Math.floor(responseTime)} { 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 ( Click to upload diff --git a/Client/src/Components/LanguageSelector.jsx b/Client/src/Components/LanguageSelector.jsx new file mode 100644 index 000000000..7a809065f --- /dev/null +++ b/Client/src/Components/LanguageSelector.jsx @@ -0,0 +1,138 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Box, MenuItem, Select, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import "flag-icons/css/flag-icons.min.css"; + +const LanguageSelector = () => { + const { i18n } = useTranslation(); + const theme = useTheme(); + const [language, setLanguage] = useState(i18n.language || "gb"); + + 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/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, }; 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, 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/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/Components/DeviceTicker/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx index 22ef5596d..dc5604a47 100644 --- a/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx +++ b/Client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx @@ -1,7 +1,7 @@ import { Stack, Typography, List, ListItem } from "@mui/material"; import { useTheme } from "@emotion/react"; import PulseDot from "../../../../../Components/Animated/PulseDot"; -import "/node_modules/flag-icons/css/flag-icons.min.css"; +import "flag-icons/css/flag-icons.min.css"; const BASE_BOX_PADDING_VERTICAL = 16; const BASE_BOX_PADDING_HORIZONTAL = 8; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx index 99e591750..f4e6a2b19 100644 --- a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx +++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx @@ -6,7 +6,7 @@ import maplibregl from "maplibre-gl"; import { useSelector } from "react-redux"; import buildStyle from "./buildStyle"; -const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => { +const DistributedUptimeMap = ({ width = "100%", checks }) => { const mapContainer = useRef(null); const map = useRef(null); const theme = useTheme(); @@ -87,7 +87,6 @@ const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => { ref={mapContainer} style={{ width: width, - height: height, }} /> ); diff --git a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx index 7a4c4b210..b57d0f9dc 100644 --- a/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx +++ b/Client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx @@ -24,73 +24,70 @@ const CustomToolTip = ({ active, payload, label }) => { ? payload[0]?.payload?.originalAvgResponseTime : (payload[0]?.payload?.avgResponseTime ?? 0); return ( - } - header="Response Times" - sx={{ padding: 0 }} + - - + + + - {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} - - - - + Response time + + + {Math.floor(responseTime)} - Response Time - {" "} - - {Math.floor(responseTime)} - - {" "} - ms - + {" "} + ms - - - {/* Display original value */} + + - + {/* Display original value */} + ); } return null; @@ -143,75 +140,81 @@ const DistributedUptimeResponseChart = ({ checks }) => { if (checks.length === 0) return null; return ( - } + header="Response Times" + sx={{ padding: 0 }} > - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} + minWidth={25} + height={220} > - - - - - - - - } - minTickGap={0} - axisLine={false} - tickLine={false} - height={20} - /> - } - wrapperStyle={{ pointerEvents: "none" }} - /> - - - + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + + + + + + + + } + minTickGap={0} + axisLine={false} + tickLine={false} + height={20} + /> + } + wrapperStyle={{ pointerEvents: "none" }} + /> + + + + ); }; diff --git a/Client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx b/Client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx new file mode 100644 index 000000000..233fc3c06 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx @@ -0,0 +1,14 @@ +import { Stack, Skeleton } from "@mui/material"; + +export const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx b/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx index 409e9cf89..9ee7fa873 100644 --- a/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx +++ b/Client/src/Pages/DistributedUptime/Details/Hooks/useSubscribeToDetails.jsx @@ -45,7 +45,7 @@ const getRandomDevice = () => { model: randomModel, }; }; -const useSubscribeToDetails = ({ monitorId, dateRange }) => { +const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) => { const [isLoading, setIsLoading] = useState(true); const [connectionStatus, setConnectionStatus] = useState(undefined); const [retryCount, setRetryCount] = useState(0); @@ -58,6 +58,14 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { const prevDateRangeRef = useRef(dateRange); useEffect(() => { + if (typeof monitorId === "undefined") { + return; + } + // If this page is public and not published, don't subscribe to details + if (isPublic && isPublished === false) { + return; + } + try { const cleanup = networkService.subscribeToDistributedUptimeDetails({ authToken, @@ -65,6 +73,9 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { dateRange: dateRange, normalize: true, onUpdate: (data) => { + if (isLoading === true) { + setIsLoading(false); + } if (networkError === true) { setNetworkError(false); } @@ -84,6 +95,7 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { setRetryCount(0); // Reset retry count on successful connection }, onError: () => { + setIsLoading(false); setNetworkError(true); setConnectionStatus("down"); }, @@ -91,8 +103,6 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { return cleanup; } catch (error) { setNetworkError(true); - } finally { - setIsLoading(false); } }, [ authToken, @@ -102,6 +112,7 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { setConnectionStatus, networkError, devices, + isLoading, ]); useEffect(() => { @@ -116,4 +127,4 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => { return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger]; }; -export default useSubscribeToDetails; +export { useSubscribeToDetails }; diff --git a/Client/src/Pages/DistributedUptime/Details/index.jsx b/Client/src/Pages/DistributedUptime/Details/index.jsx index ab39ec2ec..27a596af4 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"; +import SkeletonLayout from "./Components/Skeleton"; //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 @@ -33,6 +35,10 @@ const DistributedUptimeDetails = () => { { name: "Details", path: `/distributed-uptime/${monitorId}` }, ]; + if (isLoading) { + return ; + } + if (networkError) { return ( @@ -65,6 +71,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/Monitors/Components/Skeleton/index.jsx b/Client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx new file mode 100644 index 000000000..233fc3c06 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx @@ -0,0 +1,14 @@ +import { Stack, Skeleton } from "@mui/material"; + +export const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx b/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx index 1a1db1453..428791a3e 100644 --- a/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx +++ b/Client/src/Pages/DistributedUptime/Monitors/Hooks/useSubscribeToMonitors.jsx @@ -32,6 +32,10 @@ const useSubscribeToMonitors = () => { field: null, order: null, onUpdate: (data) => { + if (isLoading === true) { + setIsLoading(false); + } + const res = data.monitors; const { monitors, filteredMonitors, summary } = res; const mappedMonitors = filteredMonitors.map((monitor) => @@ -41,6 +45,9 @@ const useSubscribeToMonitors = () => { setMonitorsSummary(summary); setFilteredMonitors(mappedMonitors); }, + onError: () => { + setIsLoading(false); + }, }); return cleanup; @@ -49,8 +56,6 @@ const useSubscribeToMonitors = () => { body: error.message, }); setNetworkError(true); - } finally { - setIsLoading(false); } }, [authToken, user, getMonitorWithPercentage, theme]); return [isLoading, networkError, monitors, monitorsSummary, filteredMonitors]; diff --git a/Client/src/Pages/DistributedUptime/Monitors/index.jsx b/Client/src/Pages/DistributedUptime/Monitors/index.jsx index a3466b845..159d265a0 100644 --- a/Client/src/Pages/DistributedUptime/Monitors/index.jsx +++ b/Client/src/Pages/DistributedUptime/Monitors/index.jsx @@ -10,6 +10,7 @@ import GenericFallback from "../../../Components/GenericFallback"; import { useTheme } from "@mui/material/styles"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; import { useSubscribeToMonitors } from "./Hooks/useSubscribeToMonitors"; +import SkeletonLayout from "./Components/Skeleton"; // Constants const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }]; @@ -21,6 +22,10 @@ const DistributedUptimeMonitors = () => { const [isLoading, networkError, monitors, monitorsSummary, filteredMonitors] = useSubscribeToMonitors(); + if (isLoading) { + return ; + } + if (networkError) { return ( diff --git a/Client/src/Pages/DistributedUptime/Status/Components/Skeleton/index.jsx b/Client/src/Pages/DistributedUptime/Status/Components/Skeleton/index.jsx new file mode 100644 index 000000000..233fc3c06 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Status/Components/Skeleton/index.jsx @@ -0,0 +1,14 @@ +import { Stack, Skeleton } from "@mui/material"; + +export const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; 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..583fed69d --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Status/Hooks/useStatusPageFetchByUrl.jsx @@ -0,0 +1,37 @@ +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 [isPublished, setIsPublished] = useState(false); + 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]); + setIsPublished(statusPage?.isPublished); + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + fetchStatusPageByUrl(); + }, [authToken, url]); + + return [isLoading, networkError, statusPage, monitorId, isPublished]; +}; + +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..d91761e06 --- /dev/null +++ b/Client/src/Pages/DistributedUptime/Status/index.jsx @@ -0,0 +1,194 @@ +//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"; +import SkeletonLayout from "./Components/Skeleton"; +//Utils +import { useTheme } from "@mui/material/styles"; +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useSubscribeToDetails } from "../Details/Hooks/useSubscribeToDetails"; +import { useStatusPageFetchByUrl } from "./Hooks/useStatusPageFetchByUrl"; +import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete"; +import { useNavigate } from "react-router-dom"; +import { useLocation } from "react-router-dom"; +const DistributedUptimeStatus = () => { + const { url } = useParams(); + const location = useLocation(); + const isPublic = location.pathname.startsWith("/distributed-uptime/status/public"); + + // Local State + const [dateRange, setDateRange] = useState("day"); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + // Utils + const theme = useTheme(); + const navigate = useNavigate(); + const [ + statusPageIsLoading, + statusPageNetworkError, + statusPage, + monitorId, + isPublished, + ] = useStatusPageFetchByUrl({ + url, + }); + + const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] = + useSubscribeToDetails({ monitorId, dateRange, isPublic, isPublished }); + + const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => { + navigate("/distributed-uptime"); + }, url); + // Constants + const BREADCRUMBS = [ + { name: "Distributed Uptime", path: "/distributed-uptime" }, + { name: "details", path: `/distributed-uptime/${monitorId}` }, + { name: "status", path: `` }, + ]; + + let sx = {}; + if (isPublic) { + sx = { + paddingTop: "10vh", + paddingRight: "10vw", + paddingBottom: "10vh", + paddingLeft: "10vw", + }; + } + + // Done loading, a status page doesn't exist + if (!statusPageIsLoading && typeof statusPage === "undefined") { + return ( + + + + A public status page is not set up. + + Please contact to your administrator + + + ); + } + + // Done loading, a status page exists but is not public + if (!statusPageIsLoading && statusPage.isPublished === false) { + return ( + + + This status page is not public. + + + ); + } + + if (isLoading || statusPageIsLoading) { + return ; + } + + if (networkError || statusPageNetworkError) { + return ( + + + Network error + + Please check your connection + + ); + } + + if ( + typeof statusPage === "undefined" || + typeof monitor === "undefined" || + monitor.totalChecks === 0 + ) { + return ( + + + + There is no check history for this monitor yet. + + + ); + } + + return ( + + {!isPublic && } + + + + + + + + + +