diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css deleted file mode 100644 index e26f3ce84a5..00000000000 --- a/docs/src/css/custom.css +++ /dev/null @@ -1,830 +0,0 @@ -/** -* Any CSS included here will be global. The classic template -* bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. - */ - -/* You can override the default Infima variables here. */ - -:root { - --h1-markdown: #021526; - --h2-markdown: #3a6d8c; - --h3-markdown: #474e93; - --h4-markdown: #508c9b; - --h5-markdown: #6a9ab0; - --h6-markdown: #888888; - --hx-markdown-underline: #eeeeee; - --ifm-color-primary: #1e56e3; - --ifm-color-primary-light: #33925d; - --ifm-color-primary-lighter: #359962; - --ifm-color-primary-lightest: #3cad6e; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.29); - --ifm-color-gray-100: #e3e3e6; - --ifm-color-gray-200: #c9c9cc; - --ifm-color-gray-300: #b0b0b3; - --ifm-color-gray-400: #979799; - --ifm-color-gray-500: #7e7e80; - --ifm-color-gray-600: #656566; - --ifm-color-gray-700: #4c4c4d; - --ifm-color-gray-800: #323233; - --ifm-color-gray-900: #19191a; - --ifm-background-surface-color: var(--ifm-color-white); - --ifm-breadcrumb-color-active: var(--primary-neutral-600); - --ifm-menu-color: var(--neutral-mid-500); - --ifm-menu-color-active: #1e56e3; - --ifm-toc-link-color: var(--ifm-color-gray-600); - /* --ifm-code-font-size: 95%; */ - --ifm-code-background: #e5ecff; - --ifm-code-color: #0087ff; - --ifm-color-content: #000e33; - --ifm-heading-line-height: 1.5; - --ifm-heading-color: #353232; - --ifm-h1-font-size: 1.75rem; - --ifm-h2-font-size: 1.5rem; - --ifm-navbar-shadow: 0 1px 2px 0 #0000001a; - --ifm-navbar-search-input-background-color: var(--ifm-color-gray-100); - --ifm-navbar-search-input-color: var(--ifm-color-content); - --ifm-link-color: #1e56e3; - --ifm-button-background-color: #2e8555; - --ifm-button-background-color-dark: #205d3b; - --ifm-hover-overlay: rgba(0, 0, 0, 0.05); - --brand-color: black; - --base-neutral-0: #ffffff; - --sidebar-bg-color: #f3f4f6; - --neutral-mid-0: #1f2a37; - --neutral-mid-400: #1f2a37; - --neutral-mid-500: #6c737f; - --primary-neutral-800: #1f2a37; - --primary-neutral-600: #4d5761; - --primary-blue-600: #1e56e3; - --secondary-blue-400: #80a3ff; - --secondary-blue-500: #3970fd; - --secondary-blue-900: #001c63; - --next-prev-border-color: #e5e7eb; - --ifm-color-emphasis-100: #f4f8fb; - --ifm-color-emphasis-0: #fff; - --ifm-font-family-base: - 'Optimistic Display', system-ui, -apple-system, sans-serif; - --ifm-font-size-base: 17px; -} - -/* For readability concerns, you should choose a lighter palette in dark mode. */ - -[data-theme='dark'] { - --ifm-color-primary: #1e56e3; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); -} - -[data-theme='dark'] .themedComponent--light_NVdE { - display: initial; -} - -[data-theme='dark'] .navbar { - border-bottom: 1px solid #2d3956; -} - -/* Dark mode css */ - -html[data-theme='dark'] { - --ifm-background-color: #111927; - --ifm-background-surface-color: var(--ifm-background-color); - --ifm-menu-color: var(--neutral-mid-400); - --ifm-menu-color-active: var(--secondary-blue-900); - --ifm-toc-link-color: var(--ifm-color-gray-200); - --ifm-heading-color: #c6d6ff; - --ifm-color-primary: #1e56e3; - --ifm-color-content: var(--ifm-color-white); - --ifm-navbar-search-input-background-color: #001b66; - --ifm-navbar-search-input-placeholder-color: var(--ifm-color-gray-200); - --ifm-navbar-search-input-icon: url('data:image/svg+xml;utf8,'); - --ifm-hover-overlay: rgba(0, 0, 0, 0); - --ifm-button-background-color: #25c2a0; - --ifm-button-background-color-dark: #2e8555; - --ifm-navbar-link-color: var(--neutral-mid-400); - --brand-color: white; - --sidebar-bg-color: #161f36; - --neutral-mid-0: #ffffff; - --neutral-mid-400: #9da4ae; - --primary-neutral-600: #c4c4c4; - --primary-neutral-800: #9da4ae; - --primary-blue-600: var(--secondary-blue-900); - --secondary-blue-900: #c6d6ff; - --next-prev-border-color: #293441; - --ifm-color-emphasis-100: #1d1e30; - --ifm-color-emphasis-0: #111f3b; -} - -/* stylelint-disable docusaurus/copyright-header */ - -@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap'); - -.container > div > div:first-child { - padding: 1rem 5rem; -} - -.docusaurus-highlight-code-line { - background-color: rgb(72, 77, 91); - display: block; - margin: 0 calc(-1 * var(--ifm-pre-padding)); - padding: 0 var(--ifm-pre-padding); -} - -.breadcrumbs { - margin-bottom: 2rem; -} - -.table-of-contents { - padding: var(--ifm-toc-padding-vertical) 0; -} - -.table-of-contents li .table-of-contents__link { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.table-of-contents ul { - padding-left: 1.25rem; -} - -.table-of-contents li { - margin: var(--ifm-toc-padding-vertical) var(--ifm-toc-padding-horizontal); - margin-left: 0; -} - -.table-of-contents__link { - border-left: 0.125rem solid #0000; - font-size: 0.875rem; - padding-left: 1.25rem; - font-weight: 500; - color: var(--neutral-mid-400); -} - -.table-of-contents__link--active { - border-left: 0.125rem solid var(--secondary-blue-400); - font-weight: 600; - margin-left: 0; -} - -.table-of-contents__link:hover, -.table-of-contents__link:hover code, -.table-of-contents__link--active, -.table-of-contents__link--active code { - color: var(--neutral-mid-0); -} - -h1 { - font-size: var(--ifm-h1-font-size); - margin-bottom: 1.5rem; -} - -.theme-doc-sidebar-item-link-level-1 .menu__link, -.menu__link--sublist { - text-transform: uppercase; -} -.menu { - background-color: var(--sidebar-bg-color); -} - -.menu__link, -.menu * { - line-height: 1.5; - font-size: 0.7rem; - padding-bottom: 0; - padding-left: 0; - font-weight: 800; - background: transparent !important; -} - -.menu__link:hover { - color: var(--primary-blue-600); -} - -.menu__list { - border-bottom: 1px solid var(--next-prev-border-color); -} - -.menu__list-item { - padding: 0.5rem 0; -} - -/* Target specifically the link text */ -.menu__link { - white-space: normal !important; - word-break: break-word !important; - overflow-wrap: break-word !important; - width: 100% !important; - max-width: 100% !important; - padding-right: 1rem !important; -} - -/* Target the container of menu items */ -.menu__list-item { - width: 100% !important; - padding-right: 0 !important; -} - -/* Ensure the menu itself has proper width */ -.menu__list { - width: 100% !important; -} - -/* Target the entire menu container */ -.theme-doc-sidebar-menu { - width: 100% !important; - max-width: 100% !important; -} - -@media (min-width: 997px) { - .menu_SIkG { - flex-grow: 1; - padding: 1rem !important; - } -} - -.markdown > h2 { - --ifm-h2-font-size: 1.875rem; - margin-bottom: 0.8rem; - margin-top: calc(var(--ifm-h2-vertical-rhythm-top) * 0rem); - color: var(--h2-markdown); - border-bottom: 1px solid var(--hx-markdown-underline); - padding-bottom: 5px; -} - -.markdown > h3 { - --ifm-h3-font-size: 1.5rem; - margin-bottom: 0.8rem; - margin-top: calc(var(--ifm-h3-vertical-rhythm-top) * 0rem); - color: var(--h3-markdown); - border-bottom: 1px solid var(--hx-markdown-underline); - padding-bottom: 5px; -} - -.markdown > h4 { - color: var(--h4-markdown); - border-bottom: 1px solid var(--hx-markdown-underline); - padding-bottom: 5px; -} - -.markdown > h5 { - color: var(--h5-markdown); -} - -.markdown > h6 { - color: var(--h6-markdown); -} - -.navbar { - background-color: var(--sidebar-bg-color); - box-shadow: var(--ifm-navbar-shadow); - padding: 24px 48px; - height: auto; -} - -.navbar__item { - font-size: 0.875rem; - font-weight: 600; -} - -.navbar__brand { - font-size: 20px; -} - -.navbar__link:hover, -.navbar__link--active { - color: var(--primary-blue-600); - text-decoration: none; -} - -.navbar__items--right > .navbar__item:not(:first-of-type) { - margin-left: 0.25px; -} - -.dropdown__link:hover { - color: #2563eb; -} - -.dropdown__link--active { - background-color: transparent; -} - -.dropdown__link { - color: var(--ifm-navbar-link-color); -} - -.markdown { - --ifm-h1-vertical-rhythm-top: 3; - --ifm-h2-vertical-rhythm-top: 4.5; - --ifm-h3-vertical-rhythm-top: 2.5; - --ifm-heading-vertical-rhythm-top: 1.25; - --ifm-h1-vertical-rhythm-bottom: 1.25; -} - -.header-github-link:hover { - opacity: 0.7; -} - -.header-youtube-link:hover { - opacity: 0.7; -} - -.youtube-button { - background: linear-gradient(90deg, #ff3600 0%, #ff8100 100%); - border: none; - border-radius: 4px; - padding: 7px 21px; - color: #fff; - font-weight: bold; - font-size: 14px; - text-decoration: none; - display: inline-flex; - margin-right: 2.75rem; -} - -.github-button { - background: linear-gradient(90deg, #ff3600 0%, #ff8100 100%); - border: none; - border-radius: 4px; - padding: 7px 21px; - color: #fff; - font-weight: bold; - font-size: 14px; - text-decoration: none; - display: inline-flex; - margin-right: 2.75rem; -} - -.github-button:hover { - color: #fff; - text-decoration: none; -} - -.youtube-button:hover { - color: #fff; - text-decoration: none; -} - -.header-github-link:before { - content: ''; - width: 20px; - height: 20px; - display: flex; - background: url('/img/icons/github-dark.svg') no-repeat; - position: relative; - right: 8px; - top: 1.5px; -} - -.header-twitter-link:before { - content: ''; - width: 15px; - height: 15px; - display: flex; - background: url('/img/icons/twitter.svg') no-repeat 90% 100%; - position: relative; - right: 6px; - top: 2px; -} - -.header-youtube-link:before { - content: ''; - width: 25px; - height: 30px; - display: flex; - background: url('/img/icons/youtube.svg') no-repeat; - position: relative; - right: 8px; - top: 4.5px; -} - -.footer--dark { - --ifm-footer-background-color: #111927; -} - -.footer--dark li { - margin-bottom: 0; - line-height: normal; -} - -.footer__icon { - margin: 0; - padding: 2px; - color: #fff; -} - -.footer__icon:before { - content: ''; - display: inline-flex; - height: 16px; - width: 16px; - background-color: #fff; -} - -.footer__icon:hover:before { - background-color: var(--ifm-navbar-link-hover-color); -} - -.footer__github:before { - mask: url(/img/icons/github.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__slack:before { - mask: url(/img/icons/slack.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__facebook:before { - mask: url(/img/icons/facebook.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__instagram:before { - mask: url(/img/icons/instagram.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__twitter:before { - mask: url(/img/icons/twitter.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__news:before { - mask: url(/img/icons/source.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__contact:before { - mask: url(/img/icons/source.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__opportunities:before { - mask: url(/img/icons/opportunities.svg) no-repeat 100% 100%; - mask-size: cover; -} - -.footer__team:before { - mask: url(/img/icons/team.svg) no-repeat 100% 100%; - mask-size: cover; -} - -html[data-theme='dark'] .header-github-link:before { - background: url(/img/icons/github.svg) no-repeat; -} - -html[data-theme='dark'] .header-youtube-link:before { - background: url(/img/icons/youtube-white.svg) no-repeat; -} - -.markdown > button { - background: linear-gradient(90deg, #ff3600 0%, #ff8100 100%); - border: none; - border-radius: 5px; - padding: 16.8px 32px; - color: #fff; - font-weight: bold; - cursor: pointer; - transition: 0.8s; - font-size: 16px; -} - -.markdown > button:hover { - background: linear-gradient(90deg, #ff3600 30%, #ff8100 78%); -} - -@media (max-width: 996px) { - .navbar__item.github-button { - display: none; - } - - .github-button { - margin: var(--ifm-menu-link-padding-vertical) - var(--ifm-menu-link-padding-horizontal); - } - - .navbar__item.youtube-button { - display: none; - } - - .youtube-button { - margin: var(--ifm-menu-link-padding-vertical) - var(--ifm-menu-link-padding-horizontal); - } - - .center { - text-align: center; - } -} - -@media (max-width: 1000px) { - .navbar__items--right > .navbar__item:not(:first-of-type) { - margin-left: 0.25rem; - } - - .github-button { - margin-right: 0.5rem; - } - - .youtube-button { - margin-right: 0.5rem; - } - - .hero__title { - font-size: 2rem; - } -} - -@media (max-width: 1149px) and (min-width: 1050px) { - .navbar__items--right > .navbar__item:not(:first-of-type) { - margin-left: 1.5rem; - } - - .github-button { - margin-right: 0.5rem; - } - - .youtube-button { - margin-right: 0.5rem; - } -} - -@media (max-width: 1049px) and (min-width: 1001px) { - .navbar__items--right > .navbar__item:not(:first-of-type) { - margin-left: 0.5rem; - } - - .github-button { - margin-right: 0.5rem; - } - - .youtube-button { - margin-right: 0.5rem; - } -} - -@media (max-width: 768px) { - .container > div > div:first-child { - padding: 1rem !important; - } -} - -@media (min-width: 997px) and (max-width: 1327px) { - .container > div > div:first-child { - padding-left: 4rem !important; - padding-right: 4rem !important; - } -} - -h1, -h2, -h3, -h4, -h5, -h6 { - color: var(--secondary-blue-900); - margin: 20px 0 !important; -} - -p, -textarea { - margin-bottom: 1.25rem; - color: var(--primary-neutral-800); - font-size: 0.9375rem; - line-height: 1.625rem; - text-align: left; -} - -a { - color: #2563eb; - text-decoration: none; -} - -a:hover { - color: var(--secondary-blue-400); -} - -.custom-image { - width: 140%; - padding: 10px; - opacity: 1; -} - -.my-svg-icon path { - fill: var(--secondary-blue-900); -} - -/* Hide external link svg on Navbar */ - -.navbar__item > svg, -.navbar-sidebar svg { - position: absolute; - width: 0; - height: 0; - margin: 0; - padding: 0; - border: 0; - clip: rect(0 0 0 0); - clip-path: inset(50%); - overflow: hidden; -} - -/* Homepage */ -.homepage { - width: 100%; - max-width: 100%; -} - -/* Header Hero */ -.HeaderHero { - height: clamp(500px, 70vh, 800px); - padding-top: 20px; - display: flex; - flex-direction: column; - text-align: center; - align-items: center; - margin-top: clamp(2rem, 15vh, 8rem); -} - -.HeaderHero .title { - font-size: 3rem; - line-height: 1; - margin-bottom: 0 !important; - font-weight: 500; - left: -250px; - opacity: 1; -} - -.HeaderHero .tagline { - font-size: 1.5rem; - line-height: 1.3; - font-weight: 500; - margin-top: -7px; - opacity: 1; - left: -250px; -} - -.HeaderHero .description { - font-size: 1.2rem; - line-height: 1.3; - color: var(--primary-neutral-800); - opacity: 1; - text-align: center; -} - -.HeaderHero .buttons { - margin-top: 40px; -} - -/* Action Button */ -.ActionButton { - padding: 0.75rem 1.25rem; - text-align: center; - font-size: 1.2rem; - font-weight: var(--ifm-button-font-weight); - text-decoration: none !important; - border-bottom: none; - transition: all 0.2s ease-out; - border-radius: 0.375rem; - margin-right: 10px; -} - -.ActionButton.primary { - color: var(--base-neutral-0); - background-color: var(--ifm-button-background-color); - border: var(--ifm-button-border-width) solid var(--ifm-button-border-color); - white-space: nowrap; -} - -.ActionButton.primary:hover { - background-color: #1cbb99; -} - -.ActionButton.secondary { - color: #1c1e21; - background-color: #ebedf0; -} - -.ActionButton.secondary:hover { - background-color: #c7c7c7; -} - -.ActionButton.secondary::after { - content: '›'; - font-size: 24px; - margin-left: 5px; -} - -/* Section */ -.Section { - width: 100%; - padding: 50px 1.25rem 0; - overflow-x: hidden; - margin-bottom: 5rem; -} - -.Section.tint { - background-color: var(--ifm-hover-overlay); -} - -.Section.dark { - background-color: var(--dark); -} -/* Small Screens */ -@media only screen and (max-width: 480px) { - .ActionButton { - max-width: 100%; - width: 100%; - display: block; - white-space: nowrap; - } - .ActionButton.secondary { - margin-top: 1rem; - margin-left: auto; - } - .custom-image { - width: 80%; - padding-top: 60px; - } -} - -/* Small to Medium Screens */ -@media only screen and (max-width: 768px) { - .container > div > div:first-child { - padding: 1rem !important; - } - .center { - text-align: center; - } - .HeaderHero .title { - font-size: 60px; - } - .HeaderHero .tagline { - font-size: 30px; - } -} - -/* Medium Screens */ -@media (min-width: 481px) and (max-width: 960px) { - .ActionButton { - max-width: 100%; - width: 100%; - display: block; - white-space: nowrap; - } - .ActionButton.secondary { - margin-top: 1rem; - } - .HeaderHero .ActionButton { - margin: auto; - margin-top: 1rem; - } - .HeaderHero { - margin-top: 2rem; - } - .Section { - margin-bottom: 2rem; - padding-top: 1rem; - } -} - -/* Medium to Large Screens */ -@media (min-width: 997px) and (max-width: 1327px) { - .container > div > div:first-child { - padding-left: 4rem !important; - padding-right: 4rem !important; - } -} - -/* Large Screens */ -@media (min-width: 1200px) { - .HeaderHero { - height: 500px; - overflow: hidden; - } -} - -/* Responsive Navbar & Buttons */ -@media (max-width: 1149px) { - .navbar__items--right > .navbar__item:not(:first-of-type) { - margin-left: 0.25rem; - } - .github-button, - .youtube-button { - margin-right: 0.5rem; - } -} - -@media (max-width: 996px) { - .navbar__item.github-button, - .navbar__item.youtube-button { - display: none; - } - .center { - text-align: center; - } -} diff --git a/index.html b/index.html deleted file mode 100644 index 4375f88592f..00000000000 --- a/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - Talawa Admin - - -
- - - diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 87d0e8ab841..2d3f29da485 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -14,7 +14,8 @@ "loginPage": { "title": "Talawa Admin", "fromPalisadoes": "An open source application by Palisadoes Foundation volunteers", - "userLogin": "User Login", + "userLogin": "Login", + "loginSubHead": "Log in to your account", "adminLogin": "Admin Login", "atleast_8_char_long": "Atleast 8 Character long", "atleast_6_char_long": "Atleast 6 Character long", @@ -22,12 +23,11 @@ "lastName_invalid": "Last name should contain only lower and upper case letters", "password_invalid": "Password should contain atleast one lowercase letter, one uppercase letter, one numeric value and one special character", "email_invalid": "Email should have atleast 8 characters", - "Password_and_Confirm_password_mismatches.": "Password and Confirm password mismatches.", + "passwordMismatches": "Password and Confirm password mismatches.", "doNotOwnAnAccount": "Do not own an account?", "captchaError": "Captcha Error!", "Please_check_the_captcha": "Please, check the captcha.", "Something_went_wrong": "Something went wrong, Please try after sometime.", - "passwordMismatches": "Password and Confirm password mismatches.", "fillCorrectly": "Fill all the Details Correctly.", "successfullyRegistered": "Successfully Registered. Please wait until you will be approved.", "lowercase_check": "Atleast one lowercase letter", @@ -41,7 +41,7 @@ "register": "register", "firstName": "firstName", "lastName": "lastName", - "email": "email", + "email": "Email Address", "password": "password", "confirmPassword": "confirmPassword", "forgotPassword": "forgotPassword", @@ -55,38 +55,6 @@ "user": "user", "loading": "loading" }, - "userLoginPage": { - "title": "Talawa Admin", - "fromPalisadoes": "An open source application by Palisadoes Foundation volunteers", - "atleast_8_char_long": "Atleast 8 Character long", - "Password_and_Confirm_password_mismatches.": "Password and Confirm password mismatches.", - "doNotOwnAnAccount": "Do not own an account?", - "captchaError": "Captcha Error!", - "Please_check_the_captcha": "Please, check the captcha.", - "Something_went_wrong": "Something went wrong, Please try after sometime.", - "passwordMismatches": "Password and Confirm password mismatches.", - "fillCorrectly": "Fill all the Details Correctly.", - "successfullyRegistered": "Successfully Registered. Please wait until you will be approved.", - "userLogin": "User Login", - "afterRegister": "Successfully registered. Please wait for admin to approve your request.", - "selectOrg": "Select an organization", - "talawa_portal": "talawa_portal", - "login": "login", - "register": "register", - "firstName": "firstName", - "lastName": "lastName", - "email": "email", - "password": "password", - "confirmPassword": "confirmPassword", - "forgotPassword": "forgotPassword", - "enterEmail": "enterEmail", - "enterPassword": "enterPassword", - "talawaApiUnavailable": "talawaApiUnavailable", - "notAuthorised": "notAuthorised", - "notFound": "notFound", - "OR": "OR", - "loading": "loading" - }, "latestEvents": { "eventCardTitle": "Upcoming Events", "eventCardSeeAll": "See All", @@ -1047,7 +1015,7 @@ }, "userLogin": { "login": "Login", - "loginIntoYourAccount": "Login into your account", + "loginIntoYourAccount": "Log in to your account", "invalidDetailsMessage": "Please enter a valid email and password.", "notAuthorised": "Sorry! you are not Authorised!", "invalidCredentials": "Entered credentials are incorrect. Please enter valid credentials.", @@ -1060,6 +1028,11 @@ "talawaApiUnavailable": "talawaApiUnavailable" }, "userRegister": { + "firstName_invalid": "First name should contain only lower and upper case letters", + "lastName_invalid": "Last name should contain only lower and upper case letters", + "password_invalid": "Password should contain atleast one lowercase letter, one uppercase letter, one numeric value and one special character", + "email_invalid": "Email should have atleast 8 characters", + "passwordMismatches": "Password and Confirm password mismatches.", "enterFirstName": "Enter your first name", "enterLastName": "Enter your last name", "enterConfirmPassword": "Enter Password to confirm", diff --git a/public/locales/es/translation.json b/public/locales/es/translation.json index 068ed34ee88..35b27cee45d 100644 --- a/public/locales/es/translation.json +++ b/public/locales/es/translation.json @@ -16,7 +16,8 @@ "fromPalisadoes": "Una aplicación de código abierto de los voluntarios de la Fundación palisados", "talawa_portal": "Portal De Administración Talawa", "login": "Acceso", - "userLogin": "Inicio de sesión de usuario", + "userLogin": "Inicia sesión en tu cuenta", + "loginSubHead": "Inicio de sesión", "adminLogin": "Inicio de sesión de administrador", "register": "Registro", "firstName": "Primer nombre", @@ -29,7 +30,6 @@ "lastName_invalid": "El apellido debe contener solo letras minúsculas y mayúsculas.", "password_invalid": "La contraseña debe contener al menos una letra minúscula, una letra mayúscula, un valor numérico y un carácter especial.", "email_invalid": "El correo electrónico debe tener al menos 8 caracteres.", - "Password_and_Confirm_password_mismatches.": "Contraseña y Confirmar contraseña no coinciden.", "confirmPassword": "Confirmar contraseña", "forgotPassword": "Has olvidado tu contraseña ?", "enterEmail": "ingrese correo electrónico", @@ -55,38 +55,6 @@ "selectOrg": "Seleccione una organización", "afterRegister": "Registro exitoso. Por favor, espere a que el administrador apruebe su solicitud." }, - "userLoginPage": { - "title": "Administrador Talawa", - "fromPalisadoes": "Una aplicación de código abierto de los voluntarios de la Fundación palisados", - "talawa_portal": "Portal De Administración Talawa", - "login": "Acceso", - "register": "Registro", - "firstName": "Primer nombre", - "lastName": "Apellido", - "email": "Correo electrónico", - "password": "Clave", - "atleast_8_char_long": "Al menos 8 caracteres de largo", - "Password_and_Confirm_password_mismatches.": "Contraseña y Confirmar contraseña no coinciden.", - "confirmPassword": "Confirmar contraseña", - "forgotPassword": "Has olvidado tu contraseña ?", - "enterEmail": "ingrese correo electrónico", - "enterPassword": "introducir la contraseña", - "doNotOwnAnAccount": "¿No tienes una cuenta?", - "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Verifica también la conectividad de tu red.", - "captchaError": "¡Error de captcha!", - "Please_check_the_captcha": "Por favor, revisa el captcha.", - "Something_went_wrong": "Algo salió mal. Inténtalo después de un tiempo", - "passwordMismatches": "Contraseña y Confirmar contraseña no coinciden.", - "fillCorrectly": "Complete todos los detalles correctamente.", - "notAuthorised": "¡Lo siento! ¡No estás autorizado!", - "notFound": "¡Usuario no encontrado!", - "successfullyRegistered": "Registrado con éxito. Espere hasta que sea aprobado", - "userLogin": "Inicio de sesión de usuario", - "afterRegister": "Registrado exitosamente. Espere a que el administrador apruebe su solicitud.", - "OR": "O", - "loading": "Cargando...", - "selectOrg": "Seleccione una organización" - }, "latestEvents": { "eventCardTitle": "Próximos Eventos", "eventCardSeeAll": "Ver Todos", @@ -1019,6 +987,11 @@ "nothingToShow": "Nada que mostrar aquí." }, "userRegister": { + "firstName_invalid": "El nombre debe contener solo letras minúsculas y mayúsculas.", + "lastName_invalid": "El apellido debe contener solo letras minúsculas y mayúsculas.", + "password_invalid": "La contraseña debe contener al menos una letra minúscula, una letra mayúscula, un valor numérico y un carácter especial.", + "email_invalid": "El correo electrónico debe tener al menos 8 caracteres.", + "passwordMismatches": "Contraseña y Confirmar contraseña no coinciden.", "register": "Registro", "firstName": "Nombre de pila", "enterFirstName": "Ponga su primer nombre", diff --git a/public/locales/fr/translation.json b/public/locales/fr/translation.json index d454b58fdaa..0ddc7559c16 100644 --- a/public/locales/fr/translation.json +++ b/public/locales/fr/translation.json @@ -14,7 +14,8 @@ "loginPage": { "title": "Administrateur Talawa", "fromPalisadoes": "Une application open source réalisée par les bénévoles de la Fondation Palisadoes", - "userLogin": "Utilisateur en ligne", + "userLogin": "Connexion", + "loginSubHead": "Connexion à votre compte", "adminLogin": "Connexion administrateur", "atleast_8_char_long": "Au moins 8 caractères", "atleast_6_char_long": "Au moins 6 caractères", @@ -22,7 +23,6 @@ "lastName_invalid": "Le nom de famille ne doit contenir que des lettres minuscules et majuscules", "password_invalid": "Le mot de passe doit contenir au moins une lettre minuscule, une lettre majuscule, une valeur numérique et un caractère spécial", "email_invalid": "L'e-mail doit contenir au moins 8 caractères", - "Password_and_Confirm_password_mismatches.": "Mot de passe et Confirmer les incompatibilités de mot de passe.", "doNotOwnAnAccount": "Vous ne possédez pas de compte ?", "captchaError": "Erreur CAPTCHA!", "Please_check_the_captcha": "S'il vous plaît, vérifiez le captcha.", @@ -55,38 +55,6 @@ "user": "Utilisateur", "loading": "Chargement" }, - "userLoginPage": { - "title": "Administrateur Talawa", - "fromPalisadoes": "Une application open source réalisée par les bénévoles de la Fondation Palisadoes", - "atleast_8_char_long": "Au moins 8 caractères", - "Password_and_Confirm_password_mismatches.": "Mot de passe et Confirmer les incompatibilités de mot de passe.", - "doNotOwnAnAccount": "Vous ne possédez pas de compte ?", - "captchaError": "Erreur CAPTCHA!", - "Please_check_the_captcha": "S'il vous plaît, vérifiez le captcha.", - "Something_went_wrong": "Quelque chose s'est mal passé. Veuillez réessayer plus tard.", - "passwordMismatches": "Mot de passe et Confirmer les incompatibilités de mot de passe.", - "fillCorrectly": "Remplissez correctement tous les détails.", - "successfullyRegistered": "Enregistré avec succès. ", - "userLogin": "Utilisateur en ligne", - "afterRegister": "Enregistré avec succès. ", - "selectOrg": "Sélectionnez une organisation", - "talawa_portal": "Portail Talawa", - "login": "Connexion", - "register": "S'inscrire", - "firstName": "Prénom", - "lastName": "Nom de famille", - "email": "E-mail", - "password": "Mot de passe", - "confirmPassword": "Confirmer le mot de passe", - "forgotPassword": "Mot de passe oublié", - "enterEmail": "Entrer l'e-mail", - "enterPassword": "Entrer le mot de passe", - "talawaApiUnavailable": "API Talawa indisponible", - "notAuthorised": "Non autorisé", - "notFound": "Non trouvé", - "OR": "OU", - "loading": "Chargement" - }, "latestEvents": { "eventCardTitle": "évènements à venir", "eventCardSeeAll": "Voir tout", @@ -1019,6 +987,11 @@ "talawaApiUnavailable": "API Talawa non disponible" }, "userRegister": { + "firstName_invalid": "Le prénom ne doit contenir que des lettres minuscules et majuscules", + "lastName_invalid": "Le nom de famille ne doit contenir que des lettres minuscules et majuscules", + "password_invalid": "Le mot de passe doit contenir au moins une lettre minuscule, une lettre majuscule, une valeur numérique et un caractère spécial", + "email_invalid": "L'e-mail doit contenir au moins 8 caractères", + "passwordMismatches": "Mot de passe et Confirmer les incompatibilités de mot de passe.", "enterFirstName": "Entrez votre prénom", "enterLastName": "Entrez votre nom de famille", "enterConfirmPassword": "Entrez votre mot de passe pour confirmer", diff --git a/public/locales/hi/translation.json b/public/locales/hi/translation.json index 3187dc788ea..89cd69c2e5a 100644 --- a/public/locales/hi/translation.json +++ b/public/locales/hi/translation.json @@ -14,7 +14,8 @@ "loginPage": { "title": "तालावा व्यवस्थापक", "fromPalisadoes": "Palisadoes फाउंडेशन स्वयंसेवकों द्वारा विकसित एक ओपन-सोर्स एप्लिकेशन", - "userLogin": "उपयोगकर्ता लॉगिन", + "userLogin": "लॉगिन", + "loginSubHead": "अपने खाते में लॉगिन करें", "adminLogin": "एडमिन लॉगिन", "atleast_8_char_long": "कम से कम 8 अक्षर लंबे", "atleast_6_char_long": "कम से कम 6 अक्षर लंबे", @@ -22,7 +23,6 @@ "lastName_invalid": "अंतिम नाम केवल छोटे और बड़े अक्षरों को शामिल कर सकता है", "password_invalid": "पासवर्ड में कम से कम 1 छोटा अक्षर, 1 बड़ा अक्षर, 1 संख्या और 1 विशेष अक्षर होना चाहिए", "email_invalid": "ईमेल में कम से कम 8 अक्षर होने चाहिए", - "Password_and_Confirm_password_mismatches.": "पासवर्ड और पुष्टि पासवर्ड मेल नहीं खाते।", "doNotOwnAnAccount": "कोई खाता नहीं है?", "captchaError": "कैप्चा त्रुटि!", "Please_check_the_captcha": "कृपया कैप्चा जांचें।", @@ -55,38 +55,6 @@ "user": "उपयोगकर्ता", "loading": "लोड हो रहा है" }, - "userLoginPage": { - "title": "तालावा व्यवस्थापक", - "fromPalisadoes": "Palisadoes फाउंडेशन स्वयंसेवकों द्वारा विकसित एक ओपन-सोर्स एप्लिकेशन", - "atleast_8_char_long": "कम से कम 8 अक्षर लंबे", - "Password_and_Confirm_password_mismatches.": "पासवर्ड और पुष्टि पासवर्ड मेल नहीं खाते।", - "doNotOwnAnAccount": "कोई खाता नहीं है?", - "captchaError": "कैप्चा त्रुटि!", - "Please_check_the_captcha": "कृपया कैप्चा जांचें।", - "Something_went_wrong": "कुछ गलत हो गया, कृपया बाद में पुनः प्रयास करें।", - "passwordMismatches": "पासवर्ड और पुष्टि पासवर्ड मेल नहीं खाते।", - "fillCorrectly": "सभी विवरण सही ढंग से भरें।", - "successfullyRegistered": "सफलतापूर्वक पंजीकृत।", - "userLogin": "उपयोगकर्ता लॉगिन", - "afterRegister": "सफलतापूर्वक पंजीकृत।", - "selectOrg": "एक संगठन चुनें", - "talawa_portal": "तालावा पोर्टल", - "login": "लॉगिन", - "register": "पंजीकरण", - "firstName": "पहला नाम", - "lastName": "अंतिम नाम", - "email": "ईमेल", - "password": "पासवर्ड", - "confirmPassword": "पुष्टि पासवर्ड", - "forgotPassword": "पासवर्ड भूल गए", - "enterEmail": "ईमेल दर्ज करें", - "enterPassword": "पासवर्ड दर्ज करें", - "talawaApiUnavailable": "तालावा एपीआई अनुपलब्ध", - "notAuthorised": "अनधिकृत", - "notFound": "नहीं मिला", - "OR": "या", - "loading": "लोड हो रहा है" - }, "latestEvents": { "eventCardTitle": "आगामी घटनाएँ", "eventCardSeeAll": "सभी देखें", @@ -1019,6 +987,11 @@ "talawaApiUnavailable": "Talawa API अनुपलब्ध" }, "userRegister": { + "firstName_invalid": "पहला नाम केवल छोटे और बड़े अक्षरों को शामिल कर सकता है", + "lastName_invalid": "अंतिम नाम केवल छोटे और बड़े अक्षरों को शामिल कर सकता है", + "password_invalid": "पासवर्ड में कम से कम 1 छोटा अक्षर, 1 बड़ा अक्षर, 1 संख्या और 1 विशेष अक्षर होना चाहिए", + "email_invalid": "ईमेल में कम से कम 8 अक्षर होने चाहिए", + "passwordMismatches": "पासवर्ड और पुष्टि पासवर्ड मेल नहीं खाते।", "enterFirstName": "अपना पहला नाम दर्ज करें", "enterLastName": "अपना अंतिम नाम दर्ज करें", "enterConfirmPassword": "पुष्टि करने के लिए अपना पासवर्ड दर्ज करें", diff --git a/public/locales/zh/translation.json b/public/locales/zh/translation.json index 01a4709237f..a8d63acc77e 100644 --- a/public/locales/zh/translation.json +++ b/public/locales/zh/translation.json @@ -14,7 +14,8 @@ "loginPage": { "title": "塔拉瓦管理员", "fromPalisadoes": "Palisadoes 基金会志愿者开发的开源应用程序", - "userLogin": "用户登录", + "userLogin": "登录", + "loginSubHead": "登录你的账户", "adminLogin": "管理员登录", "atleast_8_char_long": "至少 8 个字符长", "atleast_6_char_long": "至少 6 个字符长", @@ -22,7 +23,6 @@ "lastName_invalid": "姓氏只能包含小写和大写字母", "password_invalid": "密码应至少包含1个小写字母、1个大写字母、1个数字和1个特殊字符", "email_invalid": "电子邮件应至少包含 8 个字符", - "Password_and_Confirm_password_mismatches.": "密码和确认密码不匹配。", "doNotOwnAnAccount": "没有帐户?", "captchaError": "验证码错误!", "Please_check_the_captcha": "请检查验证码。", @@ -55,38 +55,6 @@ "user": "用户", "loading": "加载中" }, - "userLoginPage": { - "title": "塔拉瓦管理员", - "fromPalisadoes": "Palisadoes 基金会志愿者开发的开源应用程序", - "atleast_8_char_long": "至少 8 个字符长", - "Password_and_Confirm_password_mismatches.": "密码和确认密码不匹配。", - "doNotOwnAnAccount": "没有帐户?", - "captchaError": "验证码错误!", - "Please_check_the_captcha": "请检查验证码。", - "Something_went_wrong": "出了点问题,请稍后再试。", - "passwordMismatches": "密码和确认密码不匹配。", - "fillCorrectly": "正确填写所有详细信息。", - "successfullyRegistered": "注册成功。", - "userLogin": "用户登录", - "afterRegister": "注册成功。", - "selectOrg": "选择一个组织", - "talawa_portal": "塔拉瓦门户", - "login": "登录", - "register": "注册", - "firstName": "名字", - "lastName": "姓氏", - "email": "电子邮件", - "password": "密码", - "confirmPassword": "确认密码", - "forgotPassword": "忘记密码", - "enterEmail": "输入电子邮件", - "enterPassword": "输入密码", - "talawaApiUnavailable": "塔拉瓦 API 不可用", - "notAuthorised": "未授权", - "notFound": "未找到", - "OR": "或", - "loading": "加载中" - }, "latestEvents": { "eventCardTitle": "即将举行的活动", "eventCardSeeAll": "查看全部", @@ -1019,6 +987,11 @@ "nothingToShow": "这里没有可显示的内容。" }, "userRegister": { + "firstName_invalid": "名字只能包含小写和大写字母", + "lastName_invalid": "姓氏只能包含小写和大写字母", + "password_invalid": "密码应至少包含1个小写字母、1个大写字母、1个数字和1个特殊字符", + "email_invalid": "电子邮件应至少包含 8 个字符", + "passwordMismatches": "密码和确认密码不匹配。", "enterFirstName": "输入您的名字", "enterLastName": "输入您的姓氏", "enterConfirmPassword": "输入您的密码进行确认", diff --git a/src/App.tsx b/src/App.tsx index 4842d01da9a..45b4b9325f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import SecuredRouteForUser from 'components/UserPortal/SecuredRouteForUser/Secur import OrganizaitionFundCampiagn from 'screens/OrganizationFundCampaign/OrganizationFundCampagins'; import { CURRENT_USER } from 'GraphQl/Queries/Queries'; import LoginPage from 'screens/LoginPage/LoginPage'; +import RegisterPage from 'screens/RegisterPage/RegisterPage'; import { usePluginRoutes, PluginRouteRenderer } from 'plugin'; import { getPluginManager } from 'plugin/manager'; import UserScreen from 'screens/UserPortal/UserScreen/UserScreen'; @@ -177,8 +178,9 @@ function App(): React.ReactElement { }> } /> - } /> + } /> } /> + } /> }> }> } /> diff --git a/src/assets/css/app.css b/src/assets/css/app.css index 847cdf91af7..f4a586bee47 100644 --- a/src/assets/css/app.css +++ b/src/assets/css/app.css @@ -1,6 +1,4 @@ @charset "UTF-8"; -@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,100;0,300;0,400;0,700;0,900;1,100;1,300;1,400;1,700;1,900&display=swap'); - /*! * Bootstrap v5.3.0 (https://getbootstrap.com/) * Copyright 2011-2023 The Bootstrap Authors @@ -75,13 +73,12 @@ --bs-white-rgb: 255, 255, 255; --bs-black-rgb: 0, 0, 0; --bs-font-sans-serif: - system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', 'Noto Sans', - 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', - 'Segoe UI Symbol', 'Noto Color Emoji'; + 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', + 'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji', + 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; - --bs-font-lato: 'Lato'; --bs-gradient: linear-gradient( 180deg, rgba(255, 255, 255, 0.15), @@ -107,11 +104,11 @@ --bs-tertiary-bg: #f8f9fa; --bs-tertiary-bg-rgb: 248, 249, 250; --bs-heading-color: inherit; - --bs-link-color: #0d6efd; - --bs-link-color-rgb: 13, 110, 253; + --bs-link-color: #83d6a6; + --bs-link-color-rgb: 131, 214, 166; --bs-link-decoration: none; - --bs-link-hover-color: #0a58ca; - --bs-link-hover-color-rgb: 10, 88, 202; + --bs-link-hover-color: #9cdeb8; + --bs-link-hover-color-rgb: 156, 222, 184; --bs-code-color: #d63384; --bs-highlight-bg: #fff3cd; --bs-border-width: 1px; @@ -414,8 +411,8 @@ a { text-decoration: none; } -a:hover { - --bs-link-color-rgb: var(--bs-link-hover-color-rgb); +a:focus-visible { + color: var(--bs-link-hover-color); } a:not([href]):not([class]), @@ -14172,9 +14169,6 @@ fieldset:disabled .btn { 5. General */ -:root { - --bs-body-font-family: Arial, Helvetica, sans-serif; -} * { margin: 0; @@ -14244,4 +14238,15 @@ input[type='file']::file-selector-button { */ +/* + 7. AUTH PAGES THEME +*/ + +.auth-theme { + --bs-link-color: #555555; + --bs-link-color-rgb: 85, 85, 85; + --bs-link-hover-color: #555555; + --bs-link-hover-color-rgb: 85, 85, 85; +} + /*# sourceMappingURL=app.css.map */ diff --git a/src/components/Advertisements/Advertisements.tsx b/src/components/Advertisements/Advertisements.tsx index 4aaea301f59..548d796b57e 100644 --- a/src/components/Advertisements/Advertisements.tsx +++ b/src/components/Advertisements/Advertisements.tsx @@ -46,7 +46,7 @@ import type { Advertisement } from 'types/Advertisement/type'; import Loader from 'components/Loader/Loader'; import { AdvertisementSkeleton } from './skeleton/AdvertisementSkeleton'; import { toast } from 'react-toastify'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; export default function Advertisements(): JSX.Element { const { orgId: currentOrgId } = useParams<{ orgId: string }>(); diff --git a/src/components/UserPortal/Register/Register.module.css b/src/components/UserPortal/Register/Register.module.css deleted file mode 100644 index 1fc2a34af26..00000000000 --- a/src/components/UserPortal/Register/Register.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.loginText { - cursor: pointer; -} - -.borderNone { - border: none; -} - -.colorWhite { - color: white; -} - -.colorPrimary { - background: #31bb6b; -} diff --git a/src/components/UserPortal/Register/Register.spec.tsx b/src/components/UserPortal/Register/Register.spec.tsx deleted file mode 100644 index 11bc34f1186..00000000000 --- a/src/components/UserPortal/Register/Register.spec.tsx +++ /dev/null @@ -1,350 +0,0 @@ -import type { SetStateAction } from 'react'; -import React, { act } from 'react'; -import { render, screen } from '@testing-library/react'; -import { MockedProvider } from '@apollo/react-testing'; -import userEvent from '@testing-library/user-event'; -import { I18nextProvider } from 'react-i18next'; -import { SIGNUP_MUTATION } from 'GraphQl/Mutations/mutations'; -import { BrowserRouter } from 'react-router'; -import { Provider } from 'react-redux'; -import { store } from 'state/store'; -import i18nForTest from 'utils/i18nForTest'; -import { StaticMockLink } from 'utils/StaticMockLink'; -import Register from './Register'; -import { toast } from 'react-toastify'; -import { vi } from 'vitest'; - -/** - * Unit tests for the Register component. - * - * 1. **Render test**: Verifies proper rendering of the Register component. - * 2. **Mode switch to Login**: Ensures that clicking the "setLoginBtn" changes mode to 'login'. - * 3. **Empty email validation**: Checks if toast.error is triggered for empty email. - * 4. **Empty password validation**: Ensures toast.error is called for empty password. - * 5. **Empty first name validation**: Ensures toast.error is called if first name is missing. - * 6. **Empty last name validation**: Verifies toast.error is triggered if last name is missing. - * 7. **Password mismatch validation**: Verifies toast.error is shown if confirm password doesn't match. - * 8. **Successful registration**: Confirms that toast.success is called when valid credentials are entered. - * - * GraphQL mock data is used for testing user registration functionality. - */ - -// GraphQL Mock Data -const MOCKS = [ - { - request: { - query: SIGNUP_MUTATION, - variables: { - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - password: 'johnDoe', - }, - }, - result: { - data: { - signUp: { - user: { - _id: '1', - }, - accessToken: 'accessToken', - refreshToken: 'refreshToken', - }, - }, - }, - }, -]; - -// Form Data -const formData = { - firstName: 'John', - lastName: 'Doe', - email: 'johndoe@gmail.com', - password: 'johnDoe', - confirmPassword: 'johnDoe', -}; - -// Additional GraphQL Mock Data for Error Handling -const ERROR_MOCKS = [ - { - request: { - query: SIGNUP_MUTATION, - variables: { - firstName: 'Error', - lastName: 'Test', - email: 'error@test.com', - password: 'password', - }, - }, - error: new Error('GraphQL error occurred'), - }, -]; - -// Static Mock Link -const link = new StaticMockLink(MOCKS, true); - -// Mock toast -vi.mock('react-toastify', () => ({ - toast: { - success: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, -})); - -describe('Testing Register Component [User Portal]', () => { - let setCurrentMode: React.Dispatch>; - let props: { setCurrentMode: React.Dispatch> }; - - async function waitForAsync(): Promise { - await act(() => new Promise((resolve) => setTimeout(resolve, 100))); - } - - beforeEach(() => { - setCurrentMode = vi.fn(); - props = { setCurrentMode }; - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - it('Component should be rendered properly', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - }); - - it('Expect the mode to be changed to Login', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - await userEvent.click(screen.getByTestId('setLoginBtn')); - - expect(setCurrentMode).toHaveBeenCalledWith('login'); - }); - - it('Expect toast.error to be called if email input is empty', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - await userEvent.click(screen.getByTestId('registerBtn')); - - expect(toast.error).toHaveBeenCalledWith('Please enter valid details.'); - }); - - it('Expect toast.error to be called if password input is empty', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - await userEvent.type(screen.getByTestId('emailInput'), formData.email); - await userEvent.click(screen.getByTestId('registerBtn')); - - expect(toast.error).toHaveBeenCalledWith('Please enter valid details.'); - }); - - it('Expect toast.error to be called if first name input is empty', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - await userEvent.type( - screen.getByTestId('passwordInput'), - formData.password, - ); - await userEvent.type(screen.getByTestId('emailInput'), formData.email); - await userEvent.click(screen.getByTestId('registerBtn')); - - expect(toast.error).toHaveBeenCalledWith('Please enter valid details.'); - }); - - it('Expect toast.error to be called if last name input is empty', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - await userEvent.type( - screen.getByTestId('passwordInput'), - formData.password, - ); - await userEvent.type(screen.getByTestId('emailInput'), formData.email); - await userEvent.type( - screen.getByTestId('firstNameInput'), - formData.firstName, - ); - await userEvent.click(screen.getByTestId('registerBtn')); - - expect(toast.error).toHaveBeenCalledWith('Please enter valid details.'); - }); - - it("Expect toast.error to be called if confirmPassword doesn't match with password", async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - await userEvent.type( - screen.getByTestId('passwordInput'), - formData.password, - ); - await userEvent.type(screen.getByTestId('emailInput'), formData.email); - await userEvent.type( - screen.getByTestId('firstNameInput'), - formData.firstName, - ); - await userEvent.type( - screen.getByTestId('lastNameInput'), - formData.lastName, - ); - await userEvent.click(screen.getByTestId('registerBtn')); - - expect(toast.error).toHaveBeenCalledWith( - "Password doesn't match. Confirm Password and try again.", - ); - }); - - it('Expect toast.success to be called if valid credentials are entered.', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - await userEvent.type( - screen.getByTestId('passwordInput'), - formData.password, - ); - await userEvent.type( - screen.getByTestId('confirmPasswordInput'), - formData.confirmPassword, - ); - await userEvent.type(screen.getByTestId('emailInput'), formData.email); - await userEvent.type( - screen.getByTestId('firstNameInput'), - formData.firstName, - ); - await userEvent.type( - screen.getByTestId('lastNameInput'), - formData.lastName, - ); - await userEvent.click(screen.getByTestId('registerBtn')); - - await waitForAsync(); - - expect(toast.success).toHaveBeenCalledWith( - 'Successfully registered. Please wait for admin to approve your request.', - ); - }); - - // Error Test Case - it('Expect toast.error to be called if GraphQL mutation fails', async () => { - render( - - - - - - - - - , - ); - - await waitForAsync(); - - // Fill out the form with error-triggering values - await userEvent.type(screen.getByTestId('passwordInput'), 'password'); - await userEvent.type( - screen.getByTestId('confirmPasswordInput'), - 'password', - ); - await userEvent.type(screen.getByTestId('emailInput'), 'error@test.com'); - await userEvent.type(screen.getByTestId('firstNameInput'), 'Error'); - await userEvent.type(screen.getByTestId('lastNameInput'), 'Test'); - await userEvent.click(screen.getByTestId('registerBtn')); - - await waitForAsync(); - - // Assert that toast.error is called with the error message - expect(toast.error).toHaveBeenCalledWith('GraphQL error occurred'); - }); -}); diff --git a/src/components/UserPortal/Register/Register.tsx b/src/components/UserPortal/Register/Register.tsx deleted file mode 100644 index 6ff0dd8d0e5..00000000000 --- a/src/components/UserPortal/Register/Register.tsx +++ /dev/null @@ -1,269 +0,0 @@ -/** - * @file Register.tsx - * @description This component provides a user registration form with fields for first name, last name, email, - * password, and confirm password. It includes validation, error handling, and integration with a GraphQL mutation - * for user registration. The component also allows switching to the login mode. - * - * @module Register - * - * @param {InterfaceRegisterProps} props - Props containing a function to change the current mode. - * - * @returns {JSX.Element} A registration form with input fields, validation, and a submit button. - * - * @remarks - * - Uses `react-bootstrap` for UI components and `@mui/icons-material` for icons. - * - Integrates with `react-toastify` for notifications and `@apollo/client` for GraphQL mutation. - * - Includes i18n support using `react-i18next`. - * - * @example - * ```tsx - * - * ``` - * - */ -import type { ChangeEvent, SetStateAction } from 'react'; -import React from 'react'; -import { Button, Form, InputGroup } from 'react-bootstrap'; -import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; -import BadgeOutlinedIcon from '@mui/icons-material/BadgeOutlined'; -import { LockOutlined } from '@mui/icons-material'; -import { useTranslation } from 'react-i18next'; -import { SIGNUP_MUTATION } from 'GraphQl/Mutations/mutations'; - -import styles from './Register.module.css'; -import { useMutation } from '@apollo/client'; -import { toast } from 'react-toastify'; -import { errorHandler } from 'utils/errorHandler'; - -interface InterfaceRegisterProps { - /** - * Function to change the current mode (e.g., from register to login). - */ - setCurrentMode: React.Dispatch>; -} - -export default function register(props: InterfaceRegisterProps): JSX.Element { - const { setCurrentMode } = props; - - // Translation hooks for user registration and common text - const { t } = useTranslation('translation', { keyPrefix: 'userRegister' }); - const { t: tCommon } = useTranslation('common'); - - /** - * Changes the mode to login when invoked. - */ - const handleModeChangeToLogin = (): void => { - setCurrentMode('login'); - }; - - // Mutation hook for user registration - const [registerMutation] = useMutation(SIGNUP_MUTATION); - - // State to manage the registration form variables - const [registerVariables, setRegisterVariables] = React.useState({ - firstName: '', - lastName: '', - email: '', - password: '', - confirmPassword: '', - }); - - /** - * Handles the registration process by validating inputs and invoking the mutation. - */ - const handleRegister = async (): Promise => { - if ( - !( - registerVariables.email && - registerVariables.password && - registerVariables.firstName && - registerVariables.lastName - ) - ) { - toast.error(t('invalidDetailsMessage') as string); // Error if fields are missing - } else if ( - registerVariables.password !== registerVariables.confirmPassword - ) { - toast.error(t('passwordNotMatch') as string); // Error if passwords do not match - } else { - try { - await registerMutation({ - variables: { - firstName: registerVariables.firstName, - lastName: registerVariables.lastName, - email: registerVariables.email, - password: registerVariables.password, - }, - }); - - toast.success(t('afterRegister') as string); // Success message - - // Reset form fields - setRegisterVariables({ - firstName: '', - lastName: '', - email: '', - password: '', - confirmPassword: '', - }); - } catch (error: unknown) { - // Handle any errors during registration - errorHandler(t, error); - } - } - }; - - /** - * Updates the state with the first name input value. - * @param e - Change event from the input element - */ - const handleFirstName = (e: ChangeEvent): void => { - const firstName = e.target.value; - setRegisterVariables({ ...registerVariables, firstName }); - }; - - /** - * Updates the state with the last name input value. - * @param e - Change event from the input element - */ - const handleLastName = (e: ChangeEvent): void => { - const lastName = e.target.value; - setRegisterVariables({ ...registerVariables, lastName }); - }; - - /** - * Updates the state with the email input value. - * @param e - Change event from the input element - */ - const handleEmailChange = (e: ChangeEvent): void => { - const email = e.target.value; - setRegisterVariables({ ...registerVariables, email }); - }; - - /** - * Updates the state with the password input value. - * @param e - Change event from the input element - */ - const handlePasswordChange = (e: ChangeEvent): void => { - const password = e.target.value; - - setRegisterVariables({ ...registerVariables, password }); - }; - - /** - * Updates the state with the confirm password input value. - * @param e - Change event from the input element - */ - const handleConfirmPasswordChange = ( - e: ChangeEvent, - ): void => { - const confirmPassword = e.target.value; - - setRegisterVariables({ ...registerVariables, confirmPassword }); - }; - - return ( - <> -

{tCommon('register')}

-
-
{tCommon('firstName')}
- - - - - - -
{tCommon('lastName')}
- - - - - - -
{tCommon('emailAddress')}
- - - - - - -
{tCommon('password')}
- - - - - - -
{tCommon('confirmPassword')}
- - - - - - -
- - -
- {t('alreadyhaveAnAccount')}{' '} - - {tCommon('login')} - -
- - ); -} diff --git a/src/screens/BlockUser/BlockUser.tsx b/src/screens/BlockUser/BlockUser.tsx index eaa18199598..8345e163d55 100644 --- a/src/screens/BlockUser/BlockUser.tsx +++ b/src/screens/BlockUser/BlockUser.tsx @@ -66,7 +66,7 @@ import type { import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBan, faUserPlus } from '@fortawesome/free-solid-svg-icons'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; const BlockUser = (): JSX.Element => { // Translation hooks for internationalization diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index b4d326b94d2..e28012564c5 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -1,343 +1,92 @@ -/** - * @file LoginPage.tsx - * @description This file contains the implementation of the Login and Registration page for the Talawa Admin application. - * It includes functionality for user authentication, password validation, reCAPTCHA verification, and organization selection. - * The page supports both admin and user roles and provides localization support. - * - * @module LoginPage - * - * @requires react - * @requires react-router-dom - * @requires react-bootstrap - * @requires react-google-recaptcha - * @requires @apollo/client - * @requires @mui/icons-material - * @requires @mui/material - * @requires react-toastify - * @requires i18next - * @requires utils/errorHandler - * @requires utils/useLocalstorage - * @requires utils/useSession - * @requires utils/i18n - * @requires GraphQl/Mutations/mutations - * @requires GraphQl/Queries/Queries - * @requires components/ChangeLanguageDropdown/ChangeLanguageDropDown - * @requires components/LoginPortalToggle/LoginPortalToggle - * @requires assets/svgs/palisadoes.svg - * @requires assets/svgs/talawa.svg - * - * @component - * @description The `loginPage` component renders a login and registration interface with the following features: - * - Login and registration forms with validation. - * - Password strength checks and visibility toggles. - * - reCAPTCHA integration for bot prevention. - * - Organization selection using an autocomplete dropdown. - * - Social media links and community branding. - * - Role-based navigation for admin and user. - * - * @returns {JSX.Element} The rendered login and registration page. - * - * @example - * ```tsx - * import LoginPage from './LoginPage'; - * - * const App = () => { - * return ; - * }; - * - * export default App; - * ``` - */ -import { useQuery, useMutation, useLazyQuery } from '@apollo/client'; -import { Check, Clear } from '@mui/icons-material'; -import type { ChangeEvent } from 'react'; -import React, { useEffect, useRef, useState } from 'react'; -import { Form } from 'react-bootstrap'; -import Button from 'react-bootstrap/Button'; -import Col from 'react-bootstrap/Col'; -import Row from 'react-bootstrap/Row'; -import ReCAPTCHA from 'react-google-recaptcha'; -import { useTranslation } from 'react-i18next'; -import { Link, useLocation, useNavigate } from 'react-router'; +import React, { useEffect, useState } from 'react'; +import { useQuery, useLazyQuery, useMutation } from '@apollo/client'; +import { useLocation, useNavigate } from 'react-router'; import { toast } from 'react-toastify'; -import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; -import { - REACT_APP_USE_RECAPTCHA, - RECAPTCHA_SITE_KEY, - BACKEND_URL, -} from 'Constant/constant'; -import { - RECAPTCHA_MUTATION, - SIGNUP_MUTATION, -} from 'GraphQl/Mutations/mutations'; -import { - ORGANIZATION_LIST_NO_MEMBERS, - SIGNIN_QUERY, - GET_COMMUNITY_DATA_PG, -} from 'GraphQl/Queries/Queries'; -import PalisadoesLogo from 'assets/svgs/palisadoes.svg?react'; -import TalawaLogo from 'assets/svgs/talawa.svg?react'; +import { Col, Row } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import LoginForm from 'shared-components/LoginForm/LoginForm'; +import AuthBranding from 'shared-components/AuthBranding/AuthBranding'; import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; -import { errorHandler } from 'utils/errorHandler'; +import { SIGNIN_QUERY, GET_COMMUNITY_DATA_PG } from 'GraphQl/Queries/Queries'; +import { RECAPTCHA_MUTATION } from 'GraphQl/Mutations/mutations'; import useLocalStorage from 'utils/useLocalstorage'; -import { socialMediaLinks } from '../../constants'; -import styles from '../../style/app-fixed.module.css'; -import type { InterfaceQueryOrganizationListObject } from 'utils/interfaces'; -import { Autocomplete, TextField } from '@mui/material'; import useSession from 'utils/useSession'; +import { errorHandler } from 'utils/errorHandler'; import i18n from 'utils/i18n'; +import TalawaLogo from 'assets/svgs/talawa.svg?react'; +import styles from 'style/app-fixed.module.css'; +import { REACT_APP_USE_RECAPTCHA } from 'Constant/constant'; -const loginPage = (): JSX.Element => { +/** + * LoginPage Component + * + * Handles user authentication for both admin and user roles + * + * @returns {JSX.Element} The rendered login page + */ +const LoginPage = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); - const { t: tCommon } = useTranslation('common'); const { t: tErrors } = useTranslation('errors'); - const navigate = useNavigate(); - + const location = useLocation(); const { getItem, setItem, removeItem } = useLocalStorage(); + const { startSession, extendSession } = useSession(); - document.title = t('title'); - - type PasswordValidation = { - lowercaseChar: boolean; - uppercaseChar: boolean; - numericValue: boolean; - specialChar: boolean; - }; - - const loginRecaptchaRef = useRef(null); - const SignupRecaptchaRef = useRef(null); - const [recaptchaToken, setRecaptchaToken] = useState(null); - const [showTab, setShowTab] = useState<'LOGIN' | 'REGISTER'>('LOGIN'); + useEffect(() => { + document.title = t('title'); + }, [t]); const [role, setRole] = useState<'admin' | 'user'>('user'); - const [isInputFocused, setIsInputFocused] = useState(false); - const [signformState, setSignFormState] = useState({ - signName: '', - signEmail: '', - signPassword: '', - cPassword: '', - signOrg: '', - }); - const [formState, setFormState] = useState({ - email: '', - password: '', - }); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = - useState(false); - const [showAlert, setShowAlert] = useState({ - lowercaseChar: true, - uppercaseChar: true, - numericValue: true, - specialChar: true, - }); - const [organizations, setOrganizations] = useState([]); const [pendingInvitationToken] = useState(() => getItem('pendingInvitationToken'), ); - const location = useLocation(); - const passwordValidationRegExp = { - lowercaseCharRegExp: new RegExp('[a-z]'), - uppercaseCharRegExp: new RegExp('[A-Z]'), - numericalValueRegExp: new RegExp('\\d'), - specialCharRegExp: new RegExp('[!@#$%^&*()_+{}\\[\\]:;<>,.?~\\\\/-]'), - }; - const handlePasswordCheck = (pass: string): void => { - setShowAlert({ - lowercaseChar: !passwordValidationRegExp.lowercaseCharRegExp.test(pass), - uppercaseChar: !passwordValidationRegExp.uppercaseCharRegExp.test(pass), - numericValue: !passwordValidationRegExp.numericalValueRegExp.test(pass), - specialChar: !passwordValidationRegExp.specialCharRegExp.test(pass), - }); - }; + const { data: communityData } = useQuery(GET_COMMUNITY_DATA_PG); + const [signin, { loading: loginLoading }] = useLazyQuery(SIGNIN_QUERY); + const [recaptcha] = useMutation(RECAPTCHA_MUTATION); useEffect(() => { - const isRegister = location.pathname === '/register'; - if (isRegister) { - setShowTab('REGISTER'); - } const isAdmin = location.pathname === '/admin'; - if (isAdmin) { - setRole('admin'); - } else { - setRole('user'); - } + setRole(isAdmin ? 'admin' : 'user'); }, [location.pathname]); useEffect(() => { const isLoggedIn = getItem('IsLoggedIn'); - if (isLoggedIn == 'TRUE') { + if (isLoggedIn === 'TRUE') { navigate(getItem('userId') !== null ? '/user/organizations' : '/orglist'); extendSession(); } }, []); - const togglePassword = (): void => setShowPassword(!showPassword); - const toggleConfirmPassword = (): void => - setShowConfirmPassword(!showConfirmPassword); - - const { data, refetch } = useQuery(GET_COMMUNITY_DATA_PG); - useEffect(() => { - refetch(); - }, [data]); - const [signin, { loading: loginLoading }] = useLazyQuery(SIGNIN_QUERY); - const [signup, { loading: signinLoading }] = useMutation(SIGNUP_MUTATION); - const [recaptcha] = useMutation(RECAPTCHA_MUTATION); - const { data: orgData } = useQuery(ORGANIZATION_LIST_NO_MEMBERS); - const { startSession, extendSession } = useSession(); - useEffect(() => { - if (orgData) { - const options = orgData.organizations.map( - (org: InterfaceQueryOrganizationListObject) => { - const tempObj: { label: string; id: string } | null = {} as { - label: string; - id: string; - }; - tempObj['label'] = `${org.name}(${org.addressLine1})`; - tempObj['id'] = org.id; - return tempObj; - }, - ); - setOrganizations(options); - } - }, [orgData]); - - useEffect(() => { - async function loadResource(): Promise { - try { - await fetch(BACKEND_URL as string); - } catch (error) { - errorHandler(t, error); - } - } - - loadResource(); - }, []); - + /** + * Verifies reCAPTCHA token if enabled + */ const verifyRecaptcha = async ( recaptchaToken: string | null, - ): Promise => { + ): Promise => { + if (REACT_APP_USE_RECAPTCHA !== 'yes') { + return true; + } + try { - if (REACT_APP_USE_RECAPTCHA !== 'yes') { - return true; - } const { data } = await recaptcha({ - variables: { - recaptchaToken, - }, + variables: { recaptchaToken }, }); - return data.recaptcha; } catch { toast.error(t('captchaError') as string); + return false; } }; - const handleCaptcha = (token: string | null): void => { - setRecaptchaToken(token); - }; - - const signupLink = async (e: ChangeEvent): Promise => { - e.preventDefault(); - - const { signName, signEmail, signPassword, cPassword } = signformState; - - const isVerified = await verifyRecaptcha(recaptchaToken); - - if (!isVerified) { - toast.error(t('Please_check_the_captcha') as string); - return; - } - - const isValidName = (value: string): boolean => { - // Allow letters, spaces, and hyphens, but not consecutive spaces or hyphens - return /^[a-zA-Z]+(?:[-\s][a-zA-Z]+)*$/.test(value.trim()); - }; - - const validatePassword = (password: string): boolean => { - const lengthCheck = new RegExp('^.{6,}$'); - return ( - lengthCheck.test(password) && - passwordValidationRegExp.lowercaseCharRegExp.test(password) && - passwordValidationRegExp.uppercaseCharRegExp.test(password) && - passwordValidationRegExp.numericalValueRegExp.test(password) && - passwordValidationRegExp.specialCharRegExp.test(password) - ); - }; - - if ( - isValidName(signName) && - signName.trim().length > 1 && - signEmail.length >= 8 && - signPassword.length > 1 && - validatePassword(signPassword) - ) { - if (cPassword == signPassword) { - try { - const { data: signUpData } = await signup({ - variables: { - ID: signformState.signOrg, - name: signName, - email: signEmail, - password: signPassword, - }, - }); - - if (signUpData) { - toast.success( - t( - role === 'admin' ? 'successfullyRegistered' : 'afterRegister', - ) as string, - ); - setShowTab('LOGIN'); - setSignFormState({ - signName: '', - signEmail: '', - signPassword: '', - cPassword: '', - signOrg: '', - }); - SignupRecaptchaRef.current?.reset(); - // If signup returned an authentication token, set session and resume pending invite - if (signUpData.signUp && signUpData.signUp.authenticationToken) { - const authToken = signUpData.signUp.authenticationToken; - setItem('token', authToken); - setItem('IsLoggedIn', 'TRUE'); - setItem('name', signUpData.signUp.user?.name || ''); - setItem('email', signUpData.signUp.user?.emailAddress || ''); - if (pendingInvitationToken) { - removeItem('pendingInvitationToken'); - startSession(); - window.location.href = `/event/invitation/${pendingInvitationToken}`; - return; - } - } - } - } catch (error) { - errorHandler(t, error); - SignupRecaptchaRef.current?.reset(); - } - } else { - toast.warn(t('passwordMismatches') as string); - } - } else { - if (!isValidName(signName)) { - toast.warn(t('name_invalid') as string); - } - if (!validatePassword(signPassword)) { - toast.warn(t('password_invalid') as string); - } - if (signEmail.length < 8) { - toast.warn(t('email_invalid') as string); - } - } - }; - - const loginLink = async (e: ChangeEvent): Promise => { - e.preventDefault(); + /** + * Handles login form submission + */ + const handleLogin = async ( + email: string, + password: string, + recaptchaToken: string | null, + ): Promise => { const isVerified = await verifyRecaptcha(recaptchaToken); - if (!isVerified) { toast.error(t('Please_check_the_captcha') as string); return; @@ -345,7 +94,7 @@ const loginPage = (): JSX.Element => { try { const { data: signInData } = await signin({ - variables: { email: formState.email, password: formState.password }, + variables: { email, password }, }); if (signInData) { @@ -356,10 +105,12 @@ const loginPage = (): JSX.Element => { const { signIn } = signInData; const { user, authenticationToken } = signIn; const isAdmin: boolean = user.role === 'administrator'; + if (role === 'admin' && !isAdmin) { toast.warn(tErrors('notAuthorised') as string); return; } + const loggedInUserId = user.id; setItem('token', authenticationToken); @@ -368,567 +119,52 @@ const loginPage = (): JSX.Element => { setItem('email', user.emailAddress); setItem('role', user.role); setItem('UserImage', user.avatarURL || ''); - // setItem('FirstName', user.firstName); - // setItem('LastName', user.lastName); - // setItem('UserImage', user.avatarURL); + if (role === 'admin') { setItem('id', loggedInUserId); } else { setItem('userId', loggedInUserId); } - // If there is a pending invitation token from the public invite flow, resume it - // We check the component state (captured on mount) rather than localStorage - // because localStorage may have been cleared by session management code. if (pendingInvitationToken) { removeItem('pendingInvitationToken'); startSession(); - // Use a full-page redirect to avoid client-side routing races window.location.href = `/event/invitation/${pendingInvitationToken}`; return; } - startSession(); - navigate(role === 'admin' ? '/orglist' : '/user/organizations'); } else { toast.warn(tErrors('notFound') as string); } } catch (error) { errorHandler(t, error); - loginRecaptchaRef.current?.reset(); } }; - const socialIconsList = socialMediaLinks.map(({ href, logo, tag }, index) => - data?.community ? ( - data.community?.[tag] && ( - - - - ) - ) : ( - - - - ), - ); - return ( - <> -
- - -
- {data?.community ? ( - - Community Logo -

{data.community.name}

-
- ) : ( - - -

{t('fromPalisadoes')}

-
- )} -
-
{socialIconsList}
- - -
- - - {/* LOGIN FORM */} -
-
-

- {/* {role === 'admin' ? tCommon('login') : t('userLogin')} */} - {role === 'admin' ? t('adminLogin') : t('userLogin')} -

- {tCommon('email')} -
- { - setFormState({ - ...formState, - email: e.target.value, - }); - }} - autoComplete="username" - data-testid="loginEmail" - data-cy="loginEmail" - /> - -
- - {tCommon('password')} - -
- { - setFormState({ - ...formState, - password: e.target.value, - }); - }} - disabled={loginLoading} - autoComplete="current-password" - data-cy="loginPassword" - /> - -
-
- - {tCommon('forgotPassword')} - -
- {REACT_APP_USE_RECAPTCHA === 'yes' ? ( -
- -
- ) : ( - <> - )} - - {location.pathname === '/admin' || ( -
-
-
- {tCommon('OR')} -
- -
- )} -
-
- {/* REGISTER FORM */} -
-
-

- {tCommon('register')} -

- - {/* */} -
- {tCommon('Name')} - { - setSignFormState({ - ...signformState, - signName: e.target.value, - }); - }} - /> -
- {/* */} - {/* -
- {tCommon('lastName')} - { - setSignFormState({ - ...signformState, - signlastName: e.target.value, - }); - }} - />dwdwdw -
- */} -
-
- {tCommon('email')} -
- { - setSignFormState({ - ...signformState, - signEmail: e.target.value.toLowerCase(), - }); - }} - /> - -
-
- -
- {tCommon('password')} -
- setIsInputFocused(true)} - onBlur={(): void => setIsInputFocused(false)} - required - value={signformState.signPassword} - onChange={(e): void => { - setSignFormState({ - ...signformState, - signPassword: e.target.value, - }); - handlePasswordCheck(e.target.value); - }} - /> - -
-
- {isInputFocused ? ( - signformState.signPassword.length < 6 ? ( -
-

- - - - {t('atleast_6_char_long')} -

-
- ) : ( -

- - - - {t('atleast_6_char_long')} -

- ) - ) : null} - - {!isInputFocused && - signformState.signPassword.length > 0 && - signformState.signPassword.length < 6 && ( -
- - - - {t('atleast_6_char_long')} -
- )} - {isInputFocused && ( -

- {showAlert.lowercaseChar ? ( - - - - ) : ( - - - - )} - {t('lowercase_check')} -

- )} - {isInputFocused && ( -

- {showAlert.uppercaseChar ? ( - - - - ) : ( - - - - )} - {t('uppercase_check')} -

- )} - {isInputFocused && ( -

- {showAlert.numericValue ? ( - - - - ) : ( - - - - )} - {t('numeric_value_check')} -

- )} - {isInputFocused && ( -

- {showAlert.specialChar ? ( - - - - ) : ( - - - - )} - {t('special_char_check')} -

- )} -
-
-
- {tCommon('confirmPassword')} -
- { - setSignFormState({ - ...signformState, - cPassword: e.target.value, - }); - }} - data-testid="cpassword" - autoComplete="new-password" - /> - -
- {signformState.cPassword.length > 0 && - signformState.signPassword !== - signformState.cPassword && ( -
- {t('Password_and_Confirm_password_mismatches.')} -
- )} -
-
- {t('selectOrg')} -
- { - setSignFormState({ - ...signformState, - signOrg: value?.id ?? '', - }); - }} - options={organizations} - renderInput={(params) => ( - - )} - /> -
-
- {REACT_APP_USE_RECAPTCHA === 'yes' ? ( -
- -
- ) : ( - <> - )} - -
-
-
- -
-
- +
+ + + + + +
+ + + + +
+ +
+
); }; -export default loginPage; +export default LoginPage; diff --git a/src/screens/OrgPost/OrgPost.tsx b/src/screens/OrgPost/OrgPost.tsx index 346d1109d5d..aca4f3dbf4b 100644 --- a/src/screens/OrgPost/OrgPost.tsx +++ b/src/screens/OrgPost/OrgPost.tsx @@ -22,7 +22,7 @@ import type { } from '../../types/Post/interface'; import CreatePostModal from './CreatePostModal'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; import { Add } from '@mui/icons-material'; /** diff --git a/src/screens/OrgPost/Posts.tsx b/src/screens/OrgPost/Posts.tsx index 4f23a5f64c6..020ac2faeb2 100644 --- a/src/screens/OrgPost/Posts.tsx +++ b/src/screens/OrgPost/Posts.tsx @@ -20,7 +20,7 @@ import type { ApolloError } from '@apollo/client'; import { Modal, Button } from 'react-bootstrap'; import Loader from 'components/Loader/Loader'; import NotFound from 'components/NotFound/NotFound'; -import PostCard from 'shared-components/postCard/PostCard'; +import PostCard from 'shared-components/PostCard/PostCard'; import type { InterfacePost, InterfacePostEdge } from 'types/Post/interface'; import type { PostNode } from 'types/Post/type'; import type { InterfacePostCard } from 'utils/interfaces'; diff --git a/src/screens/OrganizationEvents/OrganizationEvents.tsx b/src/screens/OrganizationEvents/OrganizationEvents.tsx index 224e6c0b754..41b88fbab34 100644 --- a/src/screens/OrganizationEvents/OrganizationEvents.tsx +++ b/src/screens/OrganizationEvents/OrganizationEvents.tsx @@ -35,7 +35,7 @@ import type { InterfaceEvent } from 'types/Event/interface'; import { UserRole } from 'types/Event/interface'; import type { InterfaceRecurrenceRule } from 'utils/recurrenceUtils/recurrenceTypes'; import CreateEventModal from './CreateEventModal'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; import { Button } from 'react-bootstrap'; import AddIcon from '@mui/icons-material/Add'; diff --git a/src/screens/OrganizationFunds/OrganizationFunds.tsx b/src/screens/OrganizationFunds/OrganizationFunds.tsx index be49f0cf87b..124e2b8e21a 100644 --- a/src/screens/OrganizationFunds/OrganizationFunds.tsx +++ b/src/screens/OrganizationFunds/OrganizationFunds.tsx @@ -16,7 +16,7 @@ import FundModal from './modal/FundModal'; import { FUND_LIST } from 'GraphQl/Queries/fundQueries'; import styles from 'style/app-fixed.module.css'; import type { InterfaceFundInfo } from 'utils/interfaces'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; const dataGridStyle = { borderRadius: 'var(--table-head-radius)', diff --git a/src/screens/OrganizationPeople/OrganizationPeople.tsx b/src/screens/OrganizationPeople/OrganizationPeople.tsx index 5dd37ec7987..9ebafcd44c5 100644 --- a/src/screens/OrganizationPeople/OrganizationPeople.tsx +++ b/src/screens/OrganizationPeople/OrganizationPeople.tsx @@ -71,7 +71,7 @@ import { Row, Button } from 'react-bootstrap'; import OrgPeopleListCard from 'components/OrgPeopleListCard/OrgPeopleListCard'; import Avatar from 'components/Avatar/Avatar'; import AddMember from './addMember/AddMember'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; const PAGE_SIZE = 10; interface IProcessedRow { diff --git a/src/screens/OrganizationPeople/addMember/AddMember.tsx b/src/screens/OrganizationPeople/addMember/AddMember.tsx index 87222ee0c83..291a0cf1f09 100644 --- a/src/screens/OrganizationPeople/addMember/AddMember.tsx +++ b/src/screens/OrganizationPeople/addMember/AddMember.tsx @@ -71,7 +71,7 @@ import type { InterfaceQueryOrganizationsListObject } from 'utils/interfaces'; import styles from 'style/app-fixed.module.css'; import Avatar from 'components/Avatar/Avatar'; import { TablePagination } from '@mui/material'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; import type { IEdge, IUserDetails, IQueryVariable } from './types'; const StyledTableCell = styled(TableCell)(() => ({ diff --git a/src/screens/OrganizationTags/OrganizationTags.tsx b/src/screens/OrganizationTags/OrganizationTags.tsx index b7093829e9f..287c4e78033 100644 --- a/src/screens/OrganizationTags/OrganizationTags.tsx +++ b/src/screens/OrganizationTags/OrganizationTags.tsx @@ -64,7 +64,7 @@ import { ORGANIZATION_USER_TAGS_LIST_PG } from 'GraphQl/Queries/OrganizationQuer import { CREATE_USER_TAG } from 'GraphQl/Mutations/TagMutations'; import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteScrollLoader from 'components/InfiniteScrollLoader/InfiniteScrollLoader'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; function OrganizationTags(): JSX.Element { const { t } = useTranslation('translation', { diff --git a/src/screens/OrganizationVenues/OrganizationVenues.tsx b/src/screens/OrganizationVenues/OrganizationVenues.tsx index 4f873e888bb..79233aceb23 100644 --- a/src/screens/OrganizationVenues/OrganizationVenues.tsx +++ b/src/screens/OrganizationVenues/OrganizationVenues.tsx @@ -59,7 +59,7 @@ import VenueModal from 'components/Venues/Modal/VenueModal'; import { DELETE_VENUE_MUTATION } from 'GraphQl/Mutations/VenueMutations'; import type { InterfaceQueryVenueListItem } from 'utils/interfaces'; import VenueCard from 'components/Venues/VenueCard'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; function organizationVenues(): JSX.Element { // Translation hooks for i18n support diff --git a/src/screens/PluginStore/PluginStore.tsx b/src/screens/PluginStore/PluginStore.tsx index 20dcb332507..ad8337ce95c 100644 --- a/src/screens/PluginStore/PluginStore.tsx +++ b/src/screens/PluginStore/PluginStore.tsx @@ -14,7 +14,7 @@ import { PluginList, UninstallConfirmationModal } from './components'; import { usePluginActions, usePluginFilters } from './hooks'; import { useGetAllPlugins } from 'plugin/graphql-service'; import type { IPluginMeta } from 'plugin'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; export default function PluginStore() { const { t } = useTranslation('translation', { keyPrefix: 'pluginStore' }); diff --git a/src/screens/RegisterPage/RegisterPage.spec.tsx b/src/screens/RegisterPage/RegisterPage.spec.tsx new file mode 100644 index 00000000000..17e5be41265 --- /dev/null +++ b/src/screens/RegisterPage/RegisterPage.spec.tsx @@ -0,0 +1,156 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import { I18nextProvider } from 'react-i18next'; +import i18nForTest from 'utils/i18nForTest'; +import { toast } from 'react-toastify'; +import RegisterPage from './RegisterPage'; +import { + ORGANIZATION_LIST_NO_MEMBERS, + GET_COMMUNITY_DATA_PG, +} from 'GraphQl/Queries/Queries'; +import { SIGNUP_MUTATION } from 'GraphQl/Mutations/mutations'; + +vi.mock('react-toastify', () => ({ + toast: { + success: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + info: vi.fn(), + }, +})); + +afterEach(() => { + vi.clearAllMocks(); +}); + +// Mock Data +const mocks = [ + { + request: { + query: ORGANIZATION_LIST_NO_MEMBERS, + }, + result: { + data: { + organizations: [ + { id: '1', name: 'Org 1', addressLine1: 'Address 1' }, + { id: '2', name: 'Org 2', addressLine1: 'Address 2' }, + ], + }, + }, + }, + { + request: { + query: GET_COMMUNITY_DATA_PG, + }, + result: { + data: { + community: { + id: 'community-1', + logoURL: 'https://example.com/logo.png', + logoMimeType: 'image/png', + name: 'Test Community', + websiteURL: 'https://example.com', + }, + }, + }, + }, + { + request: { + query: SIGNUP_MUTATION, + variables: { + ID: '1', + email: 'john@example.com', + password: 'Abc@1234', + name: 'John Doe', + }, + }, + result: { + data: { + signUp: { + authenticationToken: 'test-token', + user: { + id: 'user-123', + }, + }, + }, + }, + }, +]; + +// Helper Renderer +const renderComponent = () => { + return render( + + + + + + + , + ); +}; + +describe('RegisterPage Component', () => { + it('should render register page', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByTestId('register-text')).toBeInTheDocument(); + }); + }); + + it('should show community branding when available', async () => { + renderComponent(); + await waitFor(() => { + expect(screen.getByTestId('preLoginLogo')).toBeInTheDocument(); + }); + }); + + it('should submit registration successfully', async () => { + renderComponent(); + + // Fill First Name + const firstNameInput = await screen.findByPlaceholderText('First Name'); + fireEvent.change(firstNameInput, { target: { value: 'John' } }); + + // Fill Last Name + const lastNameInput = await screen.findByPlaceholderText('Last Name'); + fireEvent.change(lastNameInput, { target: { value: 'Doe' } }); + + // Fill Email + const emailInput = screen.getByPlaceholderText('Email'); + fireEvent.change(emailInput, { + target: { value: 'john@example.com' }, + }); + + // Fill Password + const passwordInput = screen.getByTestId('passwordField'); + const pwdNativeInput = passwordInput.querySelector('input'); + if (!pwdNativeInput) throw new Error('Password input not found'); + fireEvent.change(pwdNativeInput, { + target: { value: 'Abc@1234' }, + }); + + // Fill Confirm Password + const confirmPasswordInput = screen.getByTestId('cpassword'); + const confirmNativeInput = confirmPasswordInput.querySelector('input'); + if (!confirmNativeInput) + throw new Error('Confirm password input not found'); + fireEvent.change(confirmNativeInput, { + target: { value: 'Abc@1234' }, + }); + + // Select Organization + const orgSelector = await screen.findByText('Org 1(Address 1)'); + fireEvent.click(orgSelector); + + // Submit form + const submitBtn = screen.getByTestId('registrationBtn'); + fireEvent.click(submitBtn); + + await waitFor(() => { + expect(toast.success).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/screens/RegisterPage/RegisterPage.tsx b/src/screens/RegisterPage/RegisterPage.tsx new file mode 100644 index 00000000000..fddf0f552a5 --- /dev/null +++ b/src/screens/RegisterPage/RegisterPage.tsx @@ -0,0 +1,174 @@ +import React, { useEffect, useState } from 'react'; +import { useQuery, useMutation } from '@apollo/client'; +import { useNavigate, useLocation } from 'react-router'; +import { toast } from 'react-toastify'; +import { Col, Row } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import RegistrationForm from 'shared-components/RegistrationForm/RegistrationForm'; +import AuthBranding from 'shared-components/AuthBranding/AuthBranding'; +import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; +import { + ORGANIZATION_LIST_NO_MEMBERS, + GET_COMMUNITY_DATA_PG, +} from 'GraphQl/Queries/Queries'; +import { + RECAPTCHA_MUTATION, + SIGNUP_MUTATION, +} from 'GraphQl/Mutations/mutations'; +import useLocalStorage from 'utils/useLocalstorage'; +import useSession from 'utils/useSession'; +import { errorHandler } from 'utils/errorHandler'; +import type { IRegistrationData } from 'types/RegistrationForm/interface'; +import type { InterfaceQueryOrganizationListObject } from 'utils/interfaces'; +import TalawaLogo from 'assets/svgs/talawa.svg?react'; +import styles from 'style/app-fixed.module.css'; +import { REACT_APP_USE_RECAPTCHA } from 'Constant/constant'; + +/** + * RegisterPage Component + * Handles user registration for both admin and user roles + * @returns {JSX.Element} The rendered registration page + */ +const RegisterPage = (): JSX.Element => { + const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); + const navigate = useNavigate(); + const location = useLocation(); + const { getItem, setItem, removeItem } = useLocalStorage(); + const { startSession } = useSession(); + const [recaptcha] = useMutation(RECAPTCHA_MUTATION); + + useEffect(() => { + document.title = t('title'); + }, [t]); + + const [role, setRole] = useState<'admin' | 'user'>('user'); + const [organizations, setOrganizations] = useState< + Array<{ label: string; id: string }> + >([]); + const [pendingInvitationToken] = useState(() => + getItem('pendingInvitationToken'), + ); + + // GraphQL + const { data: communityData } = useQuery(GET_COMMUNITY_DATA_PG); + const { data: orgData } = useQuery(ORGANIZATION_LIST_NO_MEMBERS); + const [signup, { loading: signinLoading }] = useMutation(SIGNUP_MUTATION); + + useEffect(() => { + const isAdmin = location.pathname === '/admin/register'; + setRole(isAdmin ? 'admin' : 'user'); + }, [location.pathname]); + + useEffect(() => { + if (orgData) { + const options = orgData.organizations.map( + (org: InterfaceQueryOrganizationListObject) => ({ + label: `${org.name}(${org.addressLine1})`, + id: org.id, + }), + ); + setOrganizations(options); + } + }, [orgData]); + + // Handles registration form submission + const handleRegistration = async ( + userData: IRegistrationData, + recaptchaToken: string | null, + ): Promise => { + try { + // Verify reCAPTCHA if enabled + if (REACT_APP_USE_RECAPTCHA === 'yes') { + if (!recaptchaToken) { + toast.error(t('Please_check_the_captcha') as string); + return false; + } + + try { + const { data: recaptchaData } = await recaptcha({ + variables: { recaptchaToken }, + }); + + if (!recaptchaData?.recaptcha) { + toast.error(t('captchaError') as string); + return false; + } + } catch { + toast.error(t('captchaError') as string); + return false; + } + } + const { data: signUpData } = await signup({ + variables: { + ID: userData.organizationId, + name: `${userData.firstName} ${userData.lastName}`, + email: userData.email, + password: userData.password, + }, + }); + + if (signUpData) { + toast.success( + t( + role === 'admin' ? 'successfullyRegistered' : 'afterRegister', + ) as string, + ); + + // Auto-login after registration if token returned + if (signUpData.signUp && signUpData.signUp.authenticationToken) { + const authToken = signUpData.signUp.authenticationToken; + setItem('token', authToken); + setItem('IsLoggedIn', 'TRUE'); + setItem('name', `${userData.firstName} ${userData.lastName}`); + setItem('email', userData.email); + + if (pendingInvitationToken) { + removeItem('pendingInvitationToken'); + startSession(); + window.location.href = `/event/invitation/${pendingInvitationToken}`; + return true; + } + } + + // Navigate to login + navigate(role === 'admin' ? '/admin' : '/'); + return true; + } + return false; + } catch (error) { + errorHandler(t, error); + return false; + } + }; + + return ( +
+ + + + + +
+ + + + +
+ +
+
+ ); +}; + +export default RegisterPage; diff --git a/src/screens/Requests/Requests.tsx b/src/screens/Requests/Requests.tsx index 0eda1ba32ac..97ef2ff983c 100644 --- a/src/screens/Requests/Requests.tsx +++ b/src/screens/Requests/Requests.tsx @@ -68,7 +68,7 @@ import { TableHead, TableRow, } from '@mui/material'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; interface InterfaceRequestsListItem { membershipRequestId: string; diff --git a/src/screens/UserPortal/Posts/Posts.tsx b/src/screens/UserPortal/Posts/Posts.tsx index d5a2e62b8e6..c141b31c45a 100644 --- a/src/screens/UserPortal/Posts/Posts.tsx +++ b/src/screens/UserPortal/Posts/Posts.tsx @@ -54,7 +54,7 @@ import { ORGANIZATION_POST_LIST_WITH_VOTES, USER_DETAILS, } from 'GraphQl/Queries/Queries'; -import PostCard from 'shared-components/postCard/PostCard'; +import PostCard from 'shared-components/PostCard/PostCard'; import type { InterfacePostCard, InterfaceQueryUserListItem, diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index 57b82867037..743c38a097a 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -78,7 +78,7 @@ import styles from 'style/app-fixed.module.css'; import useLocalStorage from 'utils/useLocalstorage'; import type { ApolloError } from '@apollo/client'; import { WarningAmberRounded } from '@mui/icons-material'; -import PageHeader from 'shared-components/Navbar/Navbar'; +import PageHeader from 'shared-components/Navbar/PageHeader'; const Users = (): JSX.Element => { const { t } = useTranslation('translation', { keyPrefix: 'users' }); diff --git a/src/shared-components/AuthBranding/AuthBranding.spec.tsx b/src/shared-components/AuthBranding/AuthBranding.spec.tsx new file mode 100644 index 00000000000..73ec8c7412f --- /dev/null +++ b/src/shared-components/AuthBranding/AuthBranding.spec.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import AuthBranding from './AuthBranding'; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('AuthBranding Component', () => { + it('should render Palisadoes logo when no community data', () => { + render(); + expect(screen.getByTestId('PalisadoesLogo')).toBeInTheDocument(); + }); + + it('should render community social link when communityData provides one', () => { + const communityData = { + logoURL: 'https://example.com/logo.png', + name: 'Test Community', + websiteURL: 'https://example.com', + facebookURL: 'https://facebook.com/customCommunity', + }; + + render(); + + const socialLink = screen.getByTestId('preLoginSocialMedia'); + + expect(socialLink).toBeInTheDocument(); + expect(socialLink).toHaveAttribute('href', communityData.facebookURL); + }); +}); diff --git a/src/shared-components/AuthBranding/AuthBranding.tsx b/src/shared-components/AuthBranding/AuthBranding.tsx new file mode 100644 index 00000000000..f5bcdff518d --- /dev/null +++ b/src/shared-components/AuthBranding/AuthBranding.tsx @@ -0,0 +1,132 @@ +import React from 'react'; +import PalisadoesLogo from 'assets/svgs/palisadoes.svg?react'; +import { InterfaceAuthBrandingProps } from 'types/AuthBranding/interface'; +import styles from 'style/app-fixed.module.css'; +import { + FacebookLogo, + LinkedInLogo, + GithubLogo, + InstagramLogo, + XLogo, + YoutubeLogo, + SlackLogo, +} from 'assets/svgs/social-icons'; +import { useTranslation } from 'react-i18next'; + +const socialMediaLinks = [ + { + tag: 'facebookURL', + href: 'https://www.facebook.com/palisadoesproject', + logo: FacebookLogo, + }, + { + tag: 'xURL', + href: 'https://X.com/palisadoesorg?lang=en', + logo: XLogo, + }, + { + tag: 'linkedInURL', + href: 'https://www.linkedin.com/company/palisadoes/', + logo: LinkedInLogo, + }, + { + tag: 'githubURL', + href: 'https://github.com/PalisadoesFoundation', + logo: GithubLogo, + }, + { + tag: 'youtubeURL', + href: 'https://www.youtube.com/@PalisadoesOrganization', + logo: YoutubeLogo, + }, + { + tag: 'slackURL', + href: 'https://www.palisadoes.org/slack', + logo: SlackLogo, + }, + { + tag: 'instagramURL', + href: 'https://www.instagram.com/palisadoes/', + logo: InstagramLogo, + }, +]; + +/** + * AuthBranding + * Displays organization branding and social media links on authentication pages. + * @param {InterfaceAuthBrandingProps} props + * @param {Object} props.communityData + * @returns {JSX.Element} + */ +const AuthBranding: React.FC = ({ + communityData, +}) => { + const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); + + const renderSocialLinks = () => + socialMediaLinks.map(({ href, logo, tag }, index) => { + if (communityData && communityData[tag]) { + return ( + + {tag} + + ); + } + + return ( + + {tag} + + ); + }); + + return ( +
+ {/* Branding Block */} + {communityData ? ( + + Community Logo +

{communityData.name}

+
+ ) : ( + + +

{t('fromPalisadoes')}

+
+ )} + + {/* Social Media Icons */} +
{renderSocialLinks()}
+
+ ); +}; + +export default AuthBranding; diff --git a/src/shared-components/LoginForm/LoginForm.spec.tsx b/src/shared-components/LoginForm/LoginForm.spec.tsx new file mode 100644 index 00000000000..2c712bb97cb --- /dev/null +++ b/src/shared-components/LoginForm/LoginForm.spec.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import { I18nextProvider } from 'react-i18next'; +import i18nForTest from 'utils/i18nForTest'; +import LoginForm from './LoginForm'; + +const mockOnSubmit = vi.fn(); + +afterEach(() => { + vi.clearAllMocks(); +}); + +const renderComponent = (props = {}) => { + return render( + + + + + , + ); +}; + +describe('LoginForm Component', () => { + it('should render login form', () => { + renderComponent(); + expect(screen.getByTestId('loginEmail')).toBeInTheDocument(); + expect(screen.getByTestId('password')).toBeInTheDocument(); + expect(screen.getByTestId('loginBtn')).toBeInTheDocument(); + }); + + it('should call onSubmit when form is submitted', async () => { + renderComponent(); + + const emailInput = screen.getByTestId('loginEmail'); + const passwordInput = screen.getByTestId('password'); + const submitButton = screen.getByTestId('loginBtn'); + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'password123' } }); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalledWith( + 'test@example.com', + 'password123', + null, + ); + }); + }); + + it('should show register link when showRegisterLink is true', () => { + renderComponent({ showRegisterLink: true }); + expect(screen.getByTestId('goToRegisterPortion')).toBeInTheDocument(); + }); + + it('should not show register link when showRegisterLink is false', () => { + renderComponent({ showRegisterLink: false }); + expect(screen.queryByTestId('goToRegisterPortion')).not.toBeInTheDocument(); + }); +}); diff --git a/src/shared-components/LoginForm/LoginForm.tsx b/src/shared-components/LoginForm/LoginForm.tsx new file mode 100644 index 00000000000..4bf25dfb49b --- /dev/null +++ b/src/shared-components/LoginForm/LoginForm.tsx @@ -0,0 +1,147 @@ +import React, { useState, useRef } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { Link } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import ReCAPTCHA from 'react-google-recaptcha'; +import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; +import PasswordField from 'shared-components/PasswordField/PasswordField'; +import { InterfaceLoginFormProps } from 'types/LoginForm/interface'; +import { REACT_APP_USE_RECAPTCHA, RECAPTCHA_SITE_KEY } from 'Constant/constant'; +import styles from 'style/app-fixed.module.css'; + +/** + * LoginForm + * Reusable login form for both admin and user portals. + * @param {InterfaceLoginFormProps} props + * @param {'admin' | 'user'} props.role + * @param {boolean} props.isLoading + * @param {Function} props.onSubmit + * @param {string} [props.initialEmail=''] + * @param {boolean} [props.showRegisterLink=true] + * @returns {JSX.Element} + */ +const LoginForm: React.FC = ({ + role, + isLoading, + onSubmit, + initialEmail = '', + showRegisterLink = true, +}) => { + const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); + const { t: tCommon } = useTranslation('common'); + const [formState, setFormState] = useState({ + email: initialEmail, + password: '', + }); + const [recaptchaToken, setRecaptchaToken] = useState(null); + const loginRecaptchaRef = useRef(null); + + const handleSubmit = async ( + e: React.FormEvent, + ): Promise => { + e.preventDefault(); + await onSubmit(formState.email, formState.password, recaptchaToken); + }; + + const handleCaptcha = (token: string | null): void => { + setRecaptchaToken(token); + }; + + return ( +
+
+

+ {role === 'admin' ? t('adminLogin') : t('userLogin')} +

+

{t('loginSubHead')}

+ + {t('email')} +
+ { + setFormState({ + ...formState, + email: e.target.value, + }); + }} + autoComplete="username" + data-testid="loginEmail" + data-cy="loginEmail" + /> + +
+ +
+ + setFormState({ ...formState, password: value }) + } + disabled={isLoading} + placeholder={tCommon('Enter Your Password')} + testId="password" + autoComplete="current-password" + /> +
+ +
+ + {tCommon('forgotPassword')} + +
+ + {REACT_APP_USE_RECAPTCHA === 'yes' && RECAPTCHA_SITE_KEY && ( +
+ +
+ )} + + + + {showRegisterLink && ( +
+
+
+ {tCommon('OR')} +
+ + {tCommon('register')} + +
+ )} +
+
+ ); +}; + +export default LoginForm; diff --git a/src/shared-components/Navbar/Navbar.spec.tsx b/src/shared-components/Navbar/PageHeader.spec.tsx similarity index 98% rename from src/shared-components/Navbar/Navbar.spec.tsx rename to src/shared-components/Navbar/PageHeader.spec.tsx index 563f15d76cf..d7e99c4be64 100644 --- a/src/shared-components/Navbar/Navbar.spec.tsx +++ b/src/shared-components/Navbar/PageHeader.spec.tsx @@ -6,7 +6,7 @@ afterEach(() => { vi.restoreAllMocks(); vi.clearAllMocks(); }); -import PageHeader from './Navbar'; +import PageHeader from './PageHeader'; describe('PageHeader Component', () => { it('renders title when provided', () => { diff --git a/src/shared-components/Navbar/Navbar.tsx b/src/shared-components/Navbar/PageHeader.tsx similarity index 89% rename from src/shared-components/Navbar/Navbar.tsx rename to src/shared-components/Navbar/PageHeader.tsx index 195c814a11c..9fbce380cfe 100644 --- a/src/shared-components/Navbar/Navbar.tsx +++ b/src/shared-components/Navbar/PageHeader.tsx @@ -62,25 +62,7 @@ import React from 'react'; import styles from 'style/app-fixed.module.css'; import SearchBar from 'shared-components/SearchBar/SearchBar'; import SortingButton from 'subComponents/SortingButton'; - -interface InterfacePageHeaderProps { - title?: string; - search?: { - placeholder: string; - onSearch: (value: string) => void; - inputTestId?: string; - buttonTestId?: string; - }; - sorting?: Array<{ - title: string; - options: { label: string; value: string | number }[]; - selected: string | number; - onChange: (value: string | number) => void; - testIdPrefix: string; - }>; - showEventTypeFilter?: boolean; - actions?: React.ReactNode; -} +import { InterfacePageHeaderProps } from 'types/Navbar/interface'; export default function PageHeader({ title, diff --git a/src/shared-components/OrganizationSelector/OrganizationSelector.spec.tsx b/src/shared-components/OrganizationSelector/OrganizationSelector.spec.tsx new file mode 100644 index 00000000000..7fa7675aad1 --- /dev/null +++ b/src/shared-components/OrganizationSelector/OrganizationSelector.spec.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import i18nForTest from 'utils/i18nForTest'; +import OrganizationSelector from './OrganizationSelector'; + +describe('OrganizationSelector Component', () => { + const mockOnChange = vi.fn(); + const mockOrganizations = [ + { label: 'Org 1', id: '1' }, + { label: 'Org 2', id: '2' }, + ]; + + afterEach(() => { + vi.clearAllMocks(); + }); + + const renderComponent = (props = {}) => { + return render( + + + , + ); + }; + + it('should render organization selector', () => { + renderComponent(); + expect(screen.getByTestId('selectOrg')).toBeInTheDocument(); + }); + + it('should call onChange when organization is selected', () => { + renderComponent(); + const autocomplete = screen.getByTestId('selectOrg'); + fireEvent.change(autocomplete, { target: { value: 'Org 1' } }); + }); +}); diff --git a/src/shared-components/OrganizationSelector/OrganizationSelector.tsx b/src/shared-components/OrganizationSelector/OrganizationSelector.tsx new file mode 100644 index 00000000000..0c03da30818 --- /dev/null +++ b/src/shared-components/OrganizationSelector/OrganizationSelector.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { Autocomplete, TextField } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { InterfaceOrganizationSelectorProps } from 'types/OrganizationSelector/interface'; + +/** + * OrganizationSelector + * Autocomplete dropdown for selecting an organization from a list. + * @param {InterfaceOrganizationSelectorProps} + * @param {Array} props.organizations + * @param {string} props.value + * @param {Function} props.onChange + * @param {boolean} [props.disabled=false] + * @param {boolean} [props.required=false] + * @returns {JSX.Element} + */ +const OrganizationSelector: React.FC = ({ + organizations, + value, + onChange, + disabled = false, + required = false, +}) => { + const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); + + const selectedOrg = organizations.find((org) => org.id === value) || null; + + return ( +
+
+ { + onChange(newValue?.id ?? ''); + }} + options={organizations} + disabled={disabled} + getOptionLabel={(option) => option.label} + isOptionEqualToValue={(option, value) => option.id === value.id} + renderInput={(params) => ( + + {t('selectOrg')} + {required && *} + + } + required={required} + /> + )} + /> +
+
+ ); +}; + +export default OrganizationSelector; diff --git a/src/shared-components/PasswordField/PasswordField.spec.tsx b/src/shared-components/PasswordField/PasswordField.spec.tsx new file mode 100644 index 00000000000..720be41d6fc --- /dev/null +++ b/src/shared-components/PasswordField/PasswordField.spec.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import PasswordField from './PasswordField'; + +describe('PasswordField Component', () => { + const mockOnChange = vi.fn(); + + afterEach(() => { + vi.clearAllMocks(); + }); + + const defaultProps = { + label: 'Password', + value: '', + onChange: mockOnChange, + }; + + it('should render password field', () => { + render(); + expect(screen.getByLabelText('Password')).toBeInTheDocument(); + }); + + it('should toggle password visibility', () => { + render(); + const input = screen.getByTestId('passwordField'); + const toggleButton = screen.getByTestId('passwordField-toggle'); + + expect(input).toHaveAttribute('type', 'password'); + fireEvent.click(toggleButton); + expect(input).toHaveAttribute('type', 'text'); + }); + + it('should call onChange when input changes', () => { + render(); + const input = screen.getByTestId('passwordField'); + fireEvent.change(input, { target: { value: 'test123' } }); + expect(mockOnChange).toHaveBeenCalledWith('test123'); + }); +}); diff --git a/src/shared-components/PasswordField/PasswordField.tsx b/src/shared-components/PasswordField/PasswordField.tsx new file mode 100644 index 00000000000..d6e75f6fa2f --- /dev/null +++ b/src/shared-components/PasswordField/PasswordField.tsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import { Form, Button } from 'react-bootstrap'; +import { InterfacePasswordFieldProps } from 'types/PasswordField/interface'; +import styles from 'style/app-fixed.module.css'; + +/** + * PasswordField + * Reusable password input field with visibility toggle (show/hide). + * @param {InterfacePasswordFieldProps} props + * @param {string} props.label + * @param {string} props.value + * @param {Function} props.onChange + * @param {boolean} [props.disabled=false] + * @param {string} [props.placeholder=''] + * @param {Function} [props.onFocus] + * @param {Function} [props.onBlur] + * @param {string} [props.testId='passwordField'] + * @param {string} [props.autoComplete='current-password'] + * @returns {JSX.Element} + */ +const PasswordField: React.FC = ({ + label, + value, + onChange, + disabled = false, + placeholder = '', + onFocus, + onBlur, + testId = 'passwordField', + autoComplete = 'current-password', +}) => { + const [showPassword, setShowPassword] = useState(false); + + const togglePassword = (): void => setShowPassword(!showPassword); + + return ( +
+ {label} +
+ onChange(e.target.value)} + disabled={disabled} + onFocus={onFocus} + onBlur={onBlur} + autoComplete={autoComplete} + data-testid={testId} + /> + +
+
+ ); +}; + +export default PasswordField; diff --git a/src/shared-components/PasswordValidator/PasswordValidator.spec.tsx b/src/shared-components/PasswordValidator/PasswordValidator.spec.tsx new file mode 100644 index 00000000000..7920f813882 --- /dev/null +++ b/src/shared-components/PasswordValidator/PasswordValidator.spec.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import i18nForTest from 'utils/i18nForTest'; +import PasswordValidator from './PasswordValidator'; + +describe('PasswordValidator Component', () => { + const defaultProps = { + password: '', + isInputFocused: false, + validation: { + lowercaseChar: true, + uppercaseChar: true, + numericValue: true, + specialChar: true, + }, + }; + + const renderComponent = (props = {}) => { + return render( + + + , + ); + }; + + it('should not display validation when password is short but input is not focused', () => { + renderComponent({ isInputFocused: false, password: 'abc' }); + expect(screen.queryByTestId('passwordCheck')).not.toBeInTheDocument(); + }); + + it('should not display validation when input is not focused', () => { + renderComponent(); + expect(screen.queryByText(/atleast_6_char_long/i)).not.toBeInTheDocument(); + }); + + it('should display length validation when focused', () => { + renderComponent({ isInputFocused: true, password: 'abc' }); + expect(screen.getByTestId('passwordCheck')).toBeInTheDocument(); + }); + + it('should show all validations when focused', () => { + renderComponent({ isInputFocused: true, password: 'Test@123' }); + expect(screen.getByText(/lowercase_check/i)).toBeInTheDocument(); + expect(screen.getByText(/uppercase_check/i)).toBeInTheDocument(); + expect(screen.getByText(/numeric_value_check/i)).toBeInTheDocument(); + expect(screen.getByText(/special_char_check/i)).toBeInTheDocument(); + }); +}); diff --git a/src/shared-components/PasswordValidator/PasswordValidator.tsx b/src/shared-components/PasswordValidator/PasswordValidator.tsx new file mode 100644 index 00000000000..eeca3d9a8f1 --- /dev/null +++ b/src/shared-components/PasswordValidator/PasswordValidator.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Check, Clear } from '@mui/icons-material'; +import { useTranslation } from 'react-i18next'; +import styles from 'style/app-fixed.module.css'; +import { InterfacePasswordValidatorProps } from 'types/PasswordValidator/interface'; +import ValidationItem from './ValidationItem'; + +/** + * PasswordValidator Component + * + * Displays real-time password validation feedback + * + * @param {InterfacePasswordValidatorProps} props - Component props + * @returns {JSX.Element} The rendered password validator + * + * @example + * + */ +const PasswordValidator: React.FC = ({ + password, + isInputFocused, + validation, +}) => { + const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); + + return ( +
+ {isInputFocused ? ( + password.length < 6 ? ( +
+

+ + + + {t('atleast_6_char_long')} +

+
+ ) : ( +

+ + + + {t('atleast_6_char_long')} +

+ ) + ) : null} + + {!isInputFocused && password.length > 0 && password.length < 6 && ( +
+ + + + {t('atleast_6_char_long')} +
+ )} + + {isInputFocused && ( + <> + + + + + + )} +
+ ); +}; + +export default PasswordValidator; diff --git a/src/shared-components/PasswordValidator/ValidationItem.spec.tsx b/src/shared-components/PasswordValidator/ValidationItem.spec.tsx new file mode 100644 index 00000000000..1eb8fcc5dfa --- /dev/null +++ b/src/shared-components/PasswordValidator/ValidationItem.spec.tsx @@ -0,0 +1,80 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import ValidationItem from './ValidationItem'; + +describe('ValidationItem Component', () => { + it('should render with failed validation (danger styling and Clear icon)', () => { + render(); + + const element = screen.getByTestId('validation-item'); + expect(element).toHaveClass('text-danger'); + expect(element).not.toHaveClass('text-success'); + expect(screen.getByText('Password must be strong')).toBeInTheDocument(); + const svg = element.querySelector('svg'); + expect(svg).toBeInTheDocument(); + expect(svg?.getAttribute('data-testid')).toBe('ClearIcon'); + }); + + it('should render with passed validation (success styling and Check icon)', () => { + render(); + + const element = screen.getByTestId('validation-item'); + expect(element).toHaveClass('text-success'); + expect(element).not.toHaveClass('text-danger'); + expect(screen.getByText('Password is strong')).toBeInTheDocument(); + const svg = element.querySelector('svg'); + expect(svg).toBeInTheDocument(); + expect(svg?.getAttribute('data-testid')).toBe('CheckIcon'); + }); + + it('should apply custom className when provided', () => { + render( + , + ); + + const element = screen.getByTestId('validation-item'); + expect(element).toHaveClass('form-text'); + expect(element).toHaveClass('text-success'); + expect(element).toHaveClass('my-custom-class'); + }); + + it('should not break when className is not provided', () => { + render(); + + const element = screen.getByTestId('validation-item'); + expect(element).toHaveClass('form-text'); + expect(element).toHaveClass('text-danger'); + }); + + it('should render different text content correctly', () => { + const { rerender } = render( + , + ); + + expect(screen.getByText('First message')).toBeInTheDocument(); + + rerender(); + expect(screen.queryByText('First message')).not.toBeInTheDocument(); + expect(screen.getByText('Second message')).toBeInTheDocument(); + }); + + it('should switch between icons based on isValid prop', () => { + const { rerender } = render( + , + ); + + let element = screen.getByTestId('validation-item'); + let svg = element.querySelector('svg'); + + expect(svg?.getAttribute('data-testid')).toBe('ClearIcon'); + + rerender(); + element = screen.getByTestId('validation-item'); + svg = element.querySelector('svg'); + expect(svg?.getAttribute('data-testid')).toBe('CheckIcon'); + }); +}); diff --git a/src/shared-components/PasswordValidator/ValidationItem.tsx b/src/shared-components/PasswordValidator/ValidationItem.tsx new file mode 100644 index 00000000000..2b5b9b47d0b --- /dev/null +++ b/src/shared-components/PasswordValidator/ValidationItem.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Check, Clear } from '@mui/icons-material'; +import { InterfaceValidationItemProps } from 'types/PasswordValidator/interface'; +/** + * ValidationItem + * Displays a single password validation requirement with check/clear icon. + * @param {InterfaceValidationItemProps} props + * @param {boolean} props.isValid + * @param {string} props.text + * @param {string} [props.className] + * @returns {JSX.Element} + */ +const ValidationItem = ({ + isValid, + text, + className, +}: InterfaceValidationItemProps) => ( +

+ {isValid ? : } + {text} +

+); + +export default ValidationItem; diff --git a/src/shared-components/RegistrationForm/RegistrationForm.spec.tsx b/src/shared-components/RegistrationForm/RegistrationForm.spec.tsx new file mode 100644 index 00000000000..839d815f6ae --- /dev/null +++ b/src/shared-components/RegistrationForm/RegistrationForm.spec.tsx @@ -0,0 +1,188 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { BrowserRouter } from 'react-router-dom'; +import { I18nextProvider } from 'react-i18next'; +import i18nForTest from 'utils/i18nForTest'; +import RegistrationForm from './RegistrationForm'; + +vi.mock('react-toastify'); + +afterEach(() => { + vi.clearAllMocks(); +}); + +const mockOnSubmit = vi.fn(); +const mockOrganizations = [ + { label: 'Org 1', id: '1' }, + { label: 'Org 2', id: '2' }, +]; + +const renderComponent = (props = {}) => { + return render( + + + + + , + ); +}; + +describe('RegistrationForm Component', () => { + it('should render registration form', () => { + renderComponent(); + expect(screen.getByTestId('register-text')).toBeInTheDocument(); + expect(screen.getByTestId('signInEmail')).toBeInTheDocument(); + expect(screen.getByTestId('passwordField')).toBeInTheDocument(); + expect(screen.getByTestId('registrationBtn')).toBeInTheDocument(); + }); + + it('should show password mismatch error', async () => { + renderComponent(); + + const passwordInput = screen.getByTestId('passwordField'); + const confirmPasswordInput = screen.getByTestId('cpassword'); + + fireEvent.change(passwordInput, { target: { value: 'Test123!' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'Test456!' } }); + + await waitFor(() => { + expect(screen.getByTestId('passwordCheck')).toBeInTheDocument(); + }); + }); + + it('should call onSubmit with valid data including organization selection', async () => { + mockOnSubmit.mockResolvedValue(true); + renderComponent(); + + const firstNameInput = screen.getByPlaceholderText(/Enter firstname/i); + const lastNameInput = screen.getByPlaceholderText(/Enter lastname/i); + const emailInput = screen.getByTestId('signInEmail'); + const passwordInput = screen.getByTestId('passwordField'); + const confirmPasswordInput = screen.getByTestId('cpassword'); + + fireEvent.change(firstNameInput, { target: { value: 'John' } }); + fireEvent.change(lastNameInput, { target: { value: 'Doe' } }); + fireEvent.change(emailInput, { target: { value: 'john@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'Test123!' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'Test123!' } }); + + const orgSelector = screen.getByTestId('organizationSelect'); + fireEvent.change(orgSelector, { + target: { value: mockOrganizations[0].id }, + }); + + const submitButton = screen.getByTestId('registrationBtn'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalledWith( + { + firstName: 'John', + lastName: 'Doe', + email: 'john@example.com', + password: 'Test123!', + confirmPassword: 'Test123!', + organizationId: mockOrganizations[0].id, + }, + null, + ); + }); + }); + + it('should reset form after successful submission', async () => { + mockOnSubmit.mockResolvedValue(true); + renderComponent(); + + const firstNameInput = screen.getByPlaceholderText(/Enter firstname/i); + const lastNameInput = screen.getByPlaceholderText(/Enter lastname/i); + const emailInput = screen.getByTestId('signInEmail'); + const passwordInput = screen.getByTestId('passwordField'); + const confirmPasswordInput = screen.getByTestId('cpassword'); + + fireEvent.change(firstNameInput, { target: { value: 'John' } }); + fireEvent.change(lastNameInput, { target: { value: 'Doe' } }); + fireEvent.change(emailInput, { target: { value: 'john@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'Test123!' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'Test123!' } }); + + const submitButton = screen.getByTestId('registrationBtn'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(firstNameInput).toHaveValue(''); + expect(lastNameInput).toHaveValue(''); + expect(emailInput).toHaveValue(''); + expect(passwordInput).toHaveValue(''); + expect(confirmPasswordInput).toHaveValue(''); + }); + }); + + it('should not reset form after failed submission', async () => { + mockOnSubmit.mockResolvedValue(false); + renderComponent(); + + const firstNameInput = screen.getByPlaceholderText(/Enter firstname/i); + const lastNameInput = screen.getByPlaceholderText(/Enter lastname/i); + const emailInput = screen.getByTestId('signInEmail'); + const passwordInput = screen.getByTestId('passwordField'); + const confirmPasswordInput = screen.getByTestId('cpassword'); + + fireEvent.change(firstNameInput, { target: { value: 'John' } }); + fireEvent.change(lastNameInput, { target: { value: 'Doe' } }); + fireEvent.change(emailInput, { target: { value: 'john@example.com' } }); + fireEvent.change(passwordInput, { target: { value: 'Test123!' } }); + fireEvent.change(confirmPasswordInput, { target: { value: 'Test123!' } }); + + const submitButton = screen.getByTestId('registrationBtn'); + fireEvent.click(submitButton); + + await waitFor(() => { + expect(mockOnSubmit).toHaveBeenCalled(); + }); + + expect(firstNameInput).toHaveValue('John'); + expect(lastNameInput).toHaveValue('Doe'); + expect(emailInput).toHaveValue('john@example.com'); + expect(passwordInput).toHaveValue('Test123!'); + expect(confirmPasswordInput).toHaveValue('Test123!'); + }); + + it('should render login link when showLoginLink is true', () => { + renderComponent({ showLoginLink: true }); + expect(screen.getByTestId('goToLoginPortion')).toBeInTheDocument(); + }); + + it('should not render login link when showLoginLink is false', () => { + renderComponent({ showLoginLink: false }); + expect(screen.queryByTestId('goToLoginPortion')).not.toBeInTheDocument(); + }); + + it('should disable form inputs when isLoading is true', () => { + renderComponent({ isLoading: true }); + + const firstNameInput = screen.getByPlaceholderText(/Enter firstname/i); + const lastNameInput = screen.getByPlaceholderText(/Enter lastname/i); + const emailInput = screen.getByTestId('signInEmail'); + const submitButton = screen.getByTestId('registrationBtn'); + + expect(firstNameInput).toBeDisabled(); + expect(lastNameInput).toBeDisabled(); + expect(emailInput).toBeDisabled(); + expect(submitButton).toBeDisabled(); + }); + + it('should convert email to lowercase', () => { + renderComponent(); + + const emailInput = screen.getByTestId('signInEmail'); + fireEvent.change(emailInput, { target: { value: 'JOHN@EXAMPLE.COM' } }); + + expect(emailInput).toHaveValue('john@example.com'); + }); +}); diff --git a/src/shared-components/RegistrationForm/RegistrationForm.tsx b/src/shared-components/RegistrationForm/RegistrationForm.tsx new file mode 100644 index 00000000000..06870413b8f --- /dev/null +++ b/src/shared-components/RegistrationForm/RegistrationForm.tsx @@ -0,0 +1,302 @@ +import React, { useState, useRef } from 'react'; +import { Form, Button, Row, Col } from 'react-bootstrap'; +import { Link } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import ReCAPTCHA from 'react-google-recaptcha'; +import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; +import { toast } from 'react-toastify'; +import PasswordField from 'shared-components/PasswordField/PasswordField'; +import PasswordValidator from 'shared-components/PasswordValidator/PasswordValidator'; +import OrganizationSelector from 'shared-components/OrganizationSelector/OrganizationSelector'; +import { + InterfaceRegistrationFormProps, + IRegistrationData, +} from 'types/RegistrationForm/interface'; +import { REACT_APP_USE_RECAPTCHA, RECAPTCHA_SITE_KEY } from 'Constant/constant'; +import styles from 'style/app-fixed.module.css'; +import { + getPasswordValidationRules, + validatePassword, +} from '../../utils/passwordValidator'; + +/** + * RegistrationForm + * Reusable registration form for both admin and user portals. + * @param {InterfaceRegistrationFormProps} props + * @param {'admin' | 'user'} props.userType + * @param {boolean} props.isLoading + * @param {Function} props.onSubmit + * @param {boolean} [props.showLoginLink=true] + * @param {Array} props.organizations + * @returns {JSX.Element} + */ +const RegistrationForm: React.FC = ({ + userType, + isLoading, + onSubmit, + showLoginLink = true, + organizations, +}) => { + const { t } = useTranslation('translation', { keyPrefix: 'userRegister' }); + const { t: tCommon } = useTranslation('common'); + + const [formState, setFormState] = useState({ + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + organizationId: '', + }); + + const [isInputFocused, setIsInputFocused] = useState(false); + const [recaptchaToken, setRecaptchaToken] = useState(null); + const signupRecaptchaRef = useRef(null); + + const [showAlert, setShowAlert] = useState({ + lowercaseChar: true, + uppercaseChar: true, + numericValue: true, + specialChar: true, + }); + + const handlePasswordCheck = (pass: string): void => { + const rules = getPasswordValidationRules(pass); + setShowAlert({ + lowercaseChar: !rules.lowercaseChar, + uppercaseChar: !rules.uppercaseChar, + numericValue: !rules.numericValue, + specialChar: !rules.specialChar, + }); + }; + + const handleCaptcha = (token: string | null): void => { + setRecaptchaToken(token); + }; + + const handleSubmit = async ( + e: React.FormEvent, + ): Promise => { + e.preventDefault(); + + // Validation + const isValidName = (value: string): boolean => { + return /^[a-zA-Z]+(?:[-\s][a-zA-Z]+)*$/.test(value.trim()); + }; + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + + if (!isValidName(formState.firstName)) { + toast.warn(t('firstName_invalid') as string); + return; + } + + if (!isValidName(formState.lastName)) { + toast.warn(t('lastName_invalid') as string); + return; + } + + if (!emailRegex.test(formState.email)) { + toast.warn(t('email_invalid') as string); + return; + } + + if (!validatePassword(formState.password)) { + toast.warn(t('password_invalid') as string); + return; + } + + if (formState.confirmPassword !== formState.password) { + toast.warn(t('passwordMismatches') as string); + return; + } + + const success = await onSubmit(formState, recaptchaToken); + + if (success) { + setFormState({ + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + organizationId: '', + }); + + signupRecaptchaRef.current?.reset(); + } + }; + + return ( +
+

+ {tCommon('register')} +

+ + + +
+ {tCommon('firstName')} + { + setFormState({ + ...formState, + firstName: e.target.value, + }); + }} + /> +
+ + + +
+ {tCommon('lastName')} + { + setFormState({ + ...formState, + lastName: e.target.value, + }); + }} + /> +
+ +
+ +
+ {tCommon('Email Address')} +
+ { + setFormState({ + ...formState, + email: e.target.value.toLowerCase(), + }); + }} + /> + +
+
+ +
+ { + setFormState({ + ...formState, + password: value, + }); + handlePasswordCheck(value); + }} + disabled={isLoading} + placeholder={tCommon('Enter your password')} + testId="passwordField" + autoComplete="new-password" + onFocus={(): void => setIsInputFocused(true)} + onBlur={(): void => setIsInputFocused(false)} + /> + +
+ +
+ { + setFormState({ + ...formState, + confirmPassword: value, + }); + }} + disabled={isLoading} + placeholder={tCommon('Confirm your password')} + testId="cpassword" + autoComplete="new-password" + /> + {formState.confirmPassword.length > 0 && + formState.password !== formState.confirmPassword && ( +
+ {t('passwordMismatches')} +
+ )} +
+ + { + setFormState({ + ...formState, + organizationId: orgId, + }); + }} + disabled={isLoading} + required={false} + /> + + {REACT_APP_USE_RECAPTCHA === 'yes' && RECAPTCHA_SITE_KEY && ( +
+ +
+ )} + + + + {showLoginLink && ( +
+ {t('alreadyhaveAnAccount')}{' '} + + {tCommon('login')} + +
+ )} + + ); +}; + +export default RegistrationForm; diff --git a/src/style/app-fixed.module.css b/src/style/app-fixed.module.css index 65d7c6cc007..bd829eade1f 100644 --- a/src/style/app-fixed.module.css +++ b/src/style/app-fixed.module.css @@ -5709,7 +5709,6 @@ form { } .updateTimeoutCardTitle { - font-family: 'Lato', sans-serif; font-weight: 600; font-size: 24px; color: var(--updateTimeoutCardTitle-color); @@ -5720,7 +5719,6 @@ form { } .updateTimeoutCurrent { - font-family: 'Lato', sans-serif; font-weight: 400; font-size: 16px; color: var(--updateTimeoutCurrent-color); @@ -5728,7 +5726,6 @@ form { } .updateTimeoutLabel { - font-family: 'Lato', sans-serif; font-weight: 400; font-size: 16px; color: var(--updateTimeoutLabel-color); @@ -5764,7 +5761,6 @@ form { height: 36px; background: var(--updateTimeoutButton-bg); border-radius: 6px; - font-family: 'Lato', sans-serif; font-weight: 500; font-size: 16px; color: var(--updateTimeoutButton-color); @@ -7213,6 +7209,13 @@ form { box-shadow: var(--hover-shadow); } +.fromPalisadoes { + color: var(--primaryText-color); + text-align: center; + margin-top: -80px; + margin-bottom: 40px; +} + .socialIcons { display: flex; gap: 16px; @@ -7265,7 +7268,7 @@ form { } .talawa_logo { - height: clamp(3rem, 8vw, 5rem); + height: clamp(3rem, 20vw, 8rem); width: auto; aspect-ratio: 1; display: block; @@ -7297,6 +7300,12 @@ form { margin-bottom: var(--spacing-lg, 1.25rem); } +.forgot_link { + text-decoration: underline; + text-underline-offset: 3px; + color: var(--secondText-color); +} + .reg_btn { font-weight: bold; background-color: var(--register-button-bg); @@ -7307,7 +7316,7 @@ form { --bs-btn-active-bg: var(--register-button-bg-active); --bs-btn-active-border-color: var(--register-button-border-active); margin-top: 1rem; - color: var(--register-button-color); + color: var(--secondText-color); margin-bottom: 1rem; width: 100%; transition: background-color 0.2s ease; @@ -7315,7 +7324,7 @@ form { } .reg_btn:hover { - color: var(--register-button-color-hover) !important; + color: var(--secondText-color) !important; box-shadow: var(--hover-shadow); } @@ -7601,8 +7610,8 @@ form { display: flex; align-items: center; gap: 0.5rem; - height: 49px; - width: 160px; + height: 40px; + width: 150px; font-size: 0.8rem; } @@ -7618,8 +7627,8 @@ form { display: flex; align-items: center; gap: 0.5rem; - height: 49px; - width: 160px; + height: 40px; + width: 150px; font-size: 0.8rem; box-shadow: 1.5px 1.5px 1.5px var(--actionsButton-box-shadow-hover); } diff --git a/src/types/AuthBranding/interface.ts b/src/types/AuthBranding/interface.ts new file mode 100644 index 00000000000..2f4b4b1ba4d --- /dev/null +++ b/src/types/AuthBranding/interface.ts @@ -0,0 +1,11 @@ +/** + * Props for the AuthBranding component + */ +export interface InterfaceAuthBrandingProps { + communityData?: { + logoURL: string; + name: string; + websiteURL: string; + [key: string]: string | undefined; + } | null; +} diff --git a/src/types/LoginForm/interface.ts b/src/types/LoginForm/interface.ts new file mode 100644 index 00000000000..d90bb9b3de0 --- /dev/null +++ b/src/types/LoginForm/interface.ts @@ -0,0 +1,14 @@ +/** + * Props for the LoginForm component + */ +export interface InterfaceLoginFormProps { + role: 'admin' | 'user'; + isLoading: boolean; + onSubmit: ( + email: string, + password: string, + recaptchaToken: string | null, + ) => Promise; + initialEmail?: string; + showRegisterLink?: boolean; +} diff --git a/src/types/Navbar/interface.ts b/src/types/Navbar/interface.ts new file mode 100644 index 00000000000..affc5b0cd25 --- /dev/null +++ b/src/types/Navbar/interface.ts @@ -0,0 +1,23 @@ +/** + * Props for the PageHeader component. + */ +import { ReactNode } from 'react'; + +export interface InterfacePageHeaderProps { + title?: string; + search?: { + placeholder: string; + onSearch: (value: string) => void; + inputTestId?: string; + buttonTestId?: string; + }; + sorting?: Array<{ + title: string; + options: { label: string; value: string | number }[]; + selected: string | number; + onChange: (value: string | number) => void; + testIdPrefix: string; + }>; + showEventTypeFilter?: boolean; + actions?: ReactNode; +} diff --git a/src/types/OrganizationSelector/interface.ts b/src/types/OrganizationSelector/interface.ts new file mode 100644 index 00000000000..72d6f64e679 --- /dev/null +++ b/src/types/OrganizationSelector/interface.ts @@ -0,0 +1,10 @@ +/** + * Props for the OrganizationSelector component + */ +export interface InterfaceOrganizationSelectorProps { + organizations: Array<{ label: string; id: string }>; + value: string; + onChange: (orgId: string) => void; + disabled?: boolean; + required?: boolean; +} diff --git a/src/types/PasswordField/interface.ts b/src/types/PasswordField/interface.ts new file mode 100644 index 00000000000..342495db150 --- /dev/null +++ b/src/types/PasswordField/interface.ts @@ -0,0 +1,14 @@ +/** + * Props for the PasswordField component + */ +export interface InterfacePasswordFieldProps { + label: string; + value: string; + onChange: (value: string) => void; + disabled?: boolean; + placeholder?: string; + onFocus?: () => void; + onBlur?: () => void; + testId?: string; + autoComplete?: string; +} diff --git a/src/types/PasswordValidator/interface.ts b/src/types/PasswordValidator/interface.ts new file mode 100644 index 00000000000..dff27ffc8a5 --- /dev/null +++ b/src/types/PasswordValidator/interface.ts @@ -0,0 +1,19 @@ +/** + * Props for the PasswordValidator component + */ +export interface InterfacePasswordValidatorProps { + password: string; + isInputFocused: boolean; + validation: { + lowercaseChar: boolean; + uppercaseChar: boolean; + numericValue: boolean; + specialChar: boolean; + }; +} + +export interface InterfaceValidationItemProps { + isValid: boolean; + text: string; + className?: string; +} diff --git a/src/types/RegistrationForm/interface.ts b/src/types/RegistrationForm/interface.ts new file mode 100644 index 00000000000..5fc7137bad0 --- /dev/null +++ b/src/types/RegistrationForm/interface.ts @@ -0,0 +1,22 @@ +/** + * Props for the RegistrationForm component + */ +export interface InterfaceRegistrationFormProps { + userType: 'admin' | 'user'; + isLoading: boolean; + onSubmit: ( + userData: IRegistrationData, + recaptchaToken: string | null, + ) => Promise; + showLoginLink?: boolean; + organizations: Array<{ label: string; id: string }>; +} + +export interface IRegistrationData { + firstName: string; + lastName: string; + email: string; + password: string; + confirmPassword: string; + organizationId?: string; +} diff --git a/src/utils/passwordValidator.ts b/src/utils/passwordValidator.ts index 4691bb8ce80..a6c3cc9e666 100644 --- a/src/utils/passwordValidator.ts +++ b/src/utils/passwordValidator.ts @@ -12,3 +12,10 @@ export const validatePassword = (password: string): boolean => { hasLowerCase ); }; + +export const getPasswordValidationRules = (password: string) => ({ + lowercaseChar: /[a-z]/.test(password), + uppercaseChar: /[A-Z]/.test(password), + numericValue: /\d/.test(password), + specialChar: /[!@#$%^&*()_+{}[\]:;<>,.?~\\/-]/.test(password), +});