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 (
- <>
-
-
-
-
- {socialIconsList}
-
-
-
-
-
- {/* LOGIN FORM */}
-
-
{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 */}
-
-
-
-
-
- >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
};
-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 (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ });
+
+ return (
+
+ );
+};
+
+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 (
+
+
{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 (
+
+ );
+};
+
+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),
+});