diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 96e80c28a5..f81f8cb9ce 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -60,3 +60,25 @@ jobs: with: path: "./coverage/lcov.info" min_coverage: 91.0 + Graphql-Inspector: + name: Runs Introspection on the github talawa-api repo on the schema.graphql file + runs-on: ubuntu-latest + + steps: + - name: Checkout the Repository + uses: actions/checkout@v3 + + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: '16.14.1' + + - name: resolve dependency + run: npm install -g @graphql-inspector/cli + + + - name: Validate Documents + run: graphql-inspector validate './src/GraphQl/**/*.ts' github:PalisadoesFoundation/talawa-api#develop:schema.graphql --token '${{secrets.REPO_READ_ONLY}}' + + + diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index e4c69334d6..054f91cfc8 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -35,3 +35,4 @@ jobs: verbose: true fail_ci_if_error: false name: '${{env.CODECOV_UNIQUE_NAME}}' + diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7004f1b086..83f5eedc32 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -28,14 +28,14 @@ jobs: - uses: actions/stale@v7 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'This issue did not get any activity in the past 10 days and will be closed in 365 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.' - stale-pr-message: 'This pull request did not get any activity in the past 10 days and will be closed in 365 days if no update occurs. Please verify it has no conflicts with the develop branch and rebase if needed. Mention it now if you need help or give permission to other people to finish your work.' - close-issue-message: 'This issue did not get any activity in the past 365 days and thus has been closed. Please check if the newest release or develop branch has it fixed. Please, create a new issue if the issue is not fixed.' - close-pr-message: 'This pull request did not get any activity in the past 365 days and thus has been closed.' + stale-issue-message: 'This issue did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please check if the develop branch has fixed it and report again or close the issue.' + stale-pr-message: 'This pull request did not get any activity in the past 10 days and will be closed in 180 days if no update occurs. Please verify it has no conflicts with the develop branch and rebase if needed. Mention it now if you need help or give permission to other people to finish your work.' + close-issue-message: 'This issue did not get any activity in the past 180 days and thus has been closed. Please check if the newest release or develop branch has it fixed. Please, create a new issue if the issue is not fixed.' + close-pr-message: 'This pull request did not get any activity in the past 180 days and thus has been closed.' stale-issue-label: 'no-issue-activity' stale-pr-label: 'no-pr-activity' days-before-stale: 10 - days-before-close: 365 + days-before-close: 180 remove-stale-when-updated: true exempt-all-milestones: true exempt-pr-labels: 'wip' diff --git a/.husky/pre-commit b/.husky/pre-commit index a4d1b38300..e4debee2be 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -3,4 +3,6 @@ npm run format:fix npm run lint:fix -npm run typecheck \ No newline at end of file +npm run typecheck + +git add . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1188bc7f48..a0f6b96927 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,7 +150,7 @@ The process of proposing a change to Talawa Admin can be summarized as: ## Internships -If you are participating in any of the various internship programs we ar members of then please read the [introduction guides on our documentation website](https://docs.talawa.io/docs/). +If you are participating in any of the various internship programs we are members of, then please read the [introduction guides on our documentation website](https://docs.talawa.io/docs/). ## Community There are many ways to communicate with the community. diff --git a/package-lock.json b/package-lock.json index 11c1eb4f57..89054407cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,12 @@ "@types/jest": "^26.0.24", "@types/jquery": "^3.5.6", "@types/node": "^12.20.16", - "@types/react-bootstrap": "^0.32.32", + "@types/react-bootstrap": "^0.32.26", "@types/react-datepicker": "^4.1.4", "@types/react-dom": "^17.0.9", "@types/react-google-recaptcha": "^2.1.5", "@types/react-modal": "^3.12.1", - "bootstrap": "^5.3.0", + "bootstrap": "^4.2.1", "dayjs": "^1.10.7", "detect-newline": "^4.0.0", "enzyme": "^3.11.0", diff --git a/public/locales/en.json b/public/locales/en.json index c17f8a7adc..8e61c0e64b 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -445,6 +445,64 @@ "membershipRequests": "Membership requests", "adminForEvents": "Admin for events", "addedAsAdmin": "User is added as admin.", - "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." + "talawaApiUnavailable": "Talawa-API service is unavailable. Kindly check your network connection and wait for a while." + }, + "userLogin": { + "login": "Login", + "forgotPassword": "Forgot Password?", + "loginIntoYourAccount": "Login into your account", + "emailAddress": "Email Address", + "enterEmail": "Enter your email address", + "password": "Password", + "enterPassword": "Enter your password", + "register": "Register", + "invalidDetailsMessage": "Please enter a valid email and password.", + "notAuthorised": "Sorry! you are not Authorised!", + "invalidCredentials": "Entered credentials are incorrect. Please enter valid credentials.", + "talawaApiUnavailable": "Talawa-API service is unavailable. Kindly check your network connection and wait for a while." + }, + "userRegister": { + "register": "Register", + "firstName": "First Name", + "enterFirstName": "Enter your first name", + "lastName": "Last Name", + "enterLastName": "Enter your last name", + "emailAddress": "Email Address", + "enterEmail": "Enter your email address", + "password": "Password", + "enterPassword": "Enter your password", + "confirmPassword": "Confirm Password", + "enterConfirmPassword": "Enter your password to confirm", + "alreadyhaveAnAccount": "Already have an account?", + "login": "Login", + "afterRegister": "Successfully registered. Please wait for admin to approve your request.", + "passwordNotMatch": "Password doesn't match. Confirm Password and try again.", + "invalidDetailsMessage": "Please enter valid details.", + "talawaApiUnavailable": "Talawa-API service is unavailable. Kindly check your network connection and wait for a while." + }, + "userNavbar": { + "talawa": "Talawa", + "home": "Home", + "people": "People", + "events": "Events", + "chat": "Chat", + "donate": "Donate", + "myTasks": "My Tasks", + "settings": "Settings", + "language": "Language", + "logout": "Logout", + "close": "Close" + }, + "userOrganizations": { + "allOrganizations": "All Organizations", + "joinedOrganizations": "Joined Organizations", + "createdOrganizations": "Created Organizations", + "search": "Search", + "nothingToShow": "Nothing to show here." + }, + "userSidebar": { + "yourOrganizations": "Your Organizations", + "noOrganizations": "You haven't joined any organization yet.", + "viewAll": "View all" } } diff --git a/public/locales/fr.json b/public/locales/fr.json index cae6384bd0..48d7172a9e 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -439,6 +439,64 @@ "membershipRequests": "Demandes d'adhésion", "adminForEvents": "Administrateur pour les événements", "addedAsAdmin": "L'utilisateur est ajouté en tant qu'administrateur.", - "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." + "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Veuillez vérifier votre connexion réseau et attendre un moment." + }, + "userLogin": { + "login": "Connexion", + "forgotPassword": "Mot de passe oublié?", + "loginIntoYourAccount": "Connectez-vous à votre compte", + "emailAddress": "Email Address", + "enterEmail": "Entrez votre adresse email", + "password": "Mot de passe", + "enterPassword": "Tapez votre mot de passe", + "register": "Enregistrer", + "invalidDetailsMessage": "Veuillez saisir un e-mail et un mot de passe valides.", + "notAuthorised": "Désolé! vous n'êtes pas autorisé !", + "invalidCredentials": "Les informations d'identification saisies sont incorrectes. Veuillez entrer des informations d'identification valides.", + "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Veuillez vérifier votre connexion réseau et attendre un moment." + }, + "userRegister": { + "register": "Enregistrer", + "firstName": "Prénom", + "enterFirstName": "Entrez votre prénom", + "lastName": "Nom de famille", + "enterLastName": "Entrez votre nom de famille", + "emailAddress": "Email Address", + "enterEmail": "Entrez votre adresse email", + "password": "Mot de passe", + "enterPassword": "Tapez votre mot de passe", + "confirmPassword": "Confirmez le mot de passe", + "enterConfirmPassword": "Entrez votre mot de passe pour confirmer", + "alreadyhaveAnAccount": "Vous avez déjà un compte?", + "login": "Connexion", + "afterRegister": "Enregistré avec succès. Veuillez attendre que l'administrateur approuve votre demande.", + "passwordNotMatch": "Le mot de passe ne correspond pas. Confirmez le mot de passe et réessayez.", + "invalidDetailsMessage": "Veuillez entrer des détails valides.", + "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Veuillez vérifier votre connexion réseau et attendre un moment." + }, + "userNavbar": { + "talawa": "Talawa", + "home": "Maison", + "people": "Personnes", + "events": "Événements", + "chat": "Discuter", + "donate": "Donner", + "myTasks": "Mes tâches", + "settings": "Paramètres", + "language": "Langue", + "logout": "Se déconnecter", + "close": "Fermer" + }, + "userOrganizations": { + "allOrganizations": "Toutes les organisations", + "joinedOrganizations": "Organisations jointes", + "createdOrganizations": "Organisations créées", + "search": "Recherche", + "nothingToShow": "Rien à montrer ici." + }, + "userSidebar": { + "yourOrganizations": "Vos organisations", + "noOrganizations": "Vous n'avez encore rejoint aucune organisation.", + "viewAll": "Voir tout" } } diff --git a/public/locales/hi.json b/public/locales/hi.json index b9c5f8c974..e69cb4ca31 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -439,6 +439,64 @@ "membershipRequests": "सदस्यता अनुरोध", "adminForEvents": "घटनाओं के लिए व्यवस्थापक", "addedAsAdmin": "उपयोगकर्ता को व्यवस्थापक के रूप में जोड़ा गया है।", - "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" + "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। कृपया अपना नेटवर्क कनेक्शन जांचें और कुछ देर प्रतीक्षा करें।" + }, + "userLogin": { + "login": "लॉगिन", + "forgotPassword": "पासवर्ड भूल गए ?", + "loginIntoYourAccount": "अपने खाते में प्रवेश करें", + "emailAddress": "ईमेल एड्रेस", + "enterEmail": "अपना ईमेल पता दर्ज करें", + "password": "पासवर्ड", + "enterPassword": "अपना पासवर्ड डालें", + "register": "रजिस्टर करें", + "invalidDetailsMessage": "कृपया एक वैध ईमेल और पासवर्ड दर्ज करें।", + "notAuthorised": "क्षमा मांगना! आप अधिकृत नहीं हैं!", + "invalidCredentials": "दर्ज क्रेडेंशियल्स गलत हैं। कृपया मान्य क्रेडेंशियल दर्ज करें।", + "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। कृपया अपना नेटवर्क कनेक्शन जांचें और कुछ देर प्रतीक्षा करें।" + }, + "userRegister": { + "register": "रजिस्टर करें", + "firstName": "पहला नाम", + "enterFirstName": "अपना पहला नाम दर्ज करें", + "lastName": "अंतिम नाम", + "enterLastName": "अपना अंतिम नाम दर्ज करें", + "emailAddress": "ईमेल एड्रेस", + "enterEmail": "अपना ईमेल पता दर्ज करें", + "password": "पासवर्ड", + "enterPassword": "अपना पासवर्ड डालें", + "confirmPassword": "पासवर्ड की पुष्टि कीजिये", + "enterConfirmPassword": "पुष्टि करने के लिए अपना पासवर्ड दर्ज करें", + "alreadyhaveAnAccount": "क्या आपके पास पहले से एक खाता मौजूद है?", + "login": "लॉगिन", + "afterRegister": "पंजीकरण सफलतापूर्वक हो गया है। कृपया आपके अनुरोध को स्वीकार करने के लिए व्यवस्थापक की प्रतीक्षा करें।", + "passwordNotMatch": "पासवर्ड मेल नहीं खाता. पासवर्ड की पुष्टि करें और पुनः प्रयास करें।", + "invalidDetailsMessage": "कृपया मान्य विवरण दर्ज करें।", + "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। कृपया अपना नेटवर्क कनेक्शन जांचें और कुछ देर प्रतीक्षा करें।" + }, + "userNavbar": { + "talawa": "तलावा", + "home": "घर", + "people": "लोग", + "events": "आयोजन", + "chat": "बातचीत", + "donate": "दान देना", + "myTasks": "मेरा काम", + "settings": "समायोजन", + "language": "भाषा", + "logout": "लॉग आउट", + "close": "बंद करना" + }, + "userOrganizations": { + "allOrganizations": "सभी संगठन", + "joinedOrganizations": "संगठन शामिल हुए", + "createdOrganizations": "संगठन बनाये गये", + "search": "खोज", + "nothingToShow": "यहां दिखाने के लिए कुछ भी नहीं है." + }, + "userSidebar": { + "yourOrganizations": "आपके संगठन", + "noOrganizations": "आप अभी तक किसी संगठन में शामिल नहीं हुए हैं.", + "viewAll": "सभी को देखें" } } diff --git a/public/locales/sp.json b/public/locales/sp.json index 4ab34e96e6..3d68c30f64 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -441,6 +441,64 @@ "membershipRequests": "Solicitudes de membresía", "adminForEvents": "Administrador de eventos", "addedAsAdmin": "El usuario se agrega como administrador.", - "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." + "talawaApiUnavailable": "El servicio Talawa-API no está disponible. Compruebe amablemente su conexión de red y espere un momento." + }, + "userLogin": { + "login": "Acceso", + "forgotPassword": "Has olvidado tu contraseña ?", + "loginIntoYourAccount": "Inicie sesión en su cuenta", + "emailAddress": "correo electrónico", + "enterEmail": "Ingrese su dirección de correo electrónico", + "password": "Contraseña", + "enterPassword": "Ingresa tu contraseña", + "register": "Registro", + "invalidDetailsMessage": "Por favor, introduzca un correo electrónico y una contraseña válidos.", + "notAuthorised": "¡Lo siento! usted no está autorizado!", + "invalidCredentials": "Las credenciales ingresadas son incorrectas. Ingrese credenciales válidas.", + "talawaApiUnavailable": "El servicio Talawa-API no está disponible. Compruebe amablemente su conexión de red y espere un momento." + }, + "userRegister": { + "register": "Registro", + "firstName": "Nombre de pila", + "enterFirstName": "Ponga su primer nombre", + "lastName": "Apellido", + "enterLastName": "Ingresa tu apellido", + "emailAddress": "correo electrónico", + "enterEmail": "Ingrese su dirección de correo electrónico", + "password": "Contraseña", + "enterPassword": "Ingresa tu contraseña", + "confirmPassword": "confirmar Contraseña", + "enterConfirmPassword": "Ingrese su contraseña para confirmar", + "alreadyhaveAnAccount": "¿Ya tienes una cuenta?", + "login": "Acceso", + "afterRegister": "Registrado exitosamente. Espere a que el administrador apruebe su solicitud.", + "passwordNotMatch": "La contraseña no coincide. Confirme la contraseña y vuelva a intentarlo.", + "invalidDetailsMessage": "Ingrese detalles válidos.", + "talawaApiUnavailable": "El servicio Talawa-API no está disponible. Compruebe amablemente su conexión de red y espere un momento." + }, + "userNavbar": { + "talawa": "Talawa", + "home": "Hogar", + "people": "Gente", + "events": "Eventos", + "chat": "Charlar", + "donate": "Donar", + "myTasks": "Mis tareas", + "settings": "Ajustes", + "language": "Idioma", + "logout": "Cerrar sesión", + "close": "Cerca" + }, + "userOrganizations": { + "allOrganizations": "Todas las organizaciones", + "joinedOrganizations": "Organizaciones unidas", + "createdOrganizations": "Organizaciones creadas", + "search": "Buscar", + "nothingToShow": "Nada que mostrar aquí." + }, + "userSidebar": { + "yourOrganizations": "Tus Organizaciones", + "noOrganizations": "Aún no te has unido a ninguna organización.", + "viewAll": "Ver todo" } } diff --git a/public/locales/zh.json b/public/locales/zh.json index efa9744fa8..41c0a08156 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -441,6 +441,64 @@ "membershipRequests": "会员申请", "adminForEvents": "事件管理员", "addedAsAdmin": "用戶被添加為管理員。", - "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" + "talawaApiUnavailable": "Talawa-API 服務不可用。 請檢查您的網絡連接並稍等片刻。" + }, + "userLogin": { + "login": "登錄", + "forgotPassword": "忘記密碼 ?", + "loginIntoYourAccount": "登錄您的賬戶", + "emailAddress": "電子郵件地址", + "enterEmail": "輸入你的電子郵箱地址", + "password": "密碼", + "enterPassword": "輸入您的密碼", + "register": "登記", + "invalidDetailsMessage": "請輸入有效的電子郵件和密碼", + "notAuthorised": "對不起! 你沒有被授權!", + "invalidCredentials": "輸入的憑據不正確。 請輸入有效憑據。", + "talawaApiUnavailable": "Talawa-API 服務不可用。 請檢查您的網絡連接並稍等片刻。" + }, + "userRegister": { + "register": "登記", + "firstName": "名", + "enterFirstName": "輸入您的名字", + "lastName": "姓", + "enterLastName": "輸入您的姓氏", + "emailAddress": "電子郵件地址", + "enterEmail": "輸入你的電子郵箱地址", + "password": "密碼", + "enterPassword": "輸入您的密碼", + "confirmPassword": "確認密碼", + "enterConfirmPassword": "輸入您的密碼以確認", + "alreadyhaveAnAccount": "已有帳戶?", + "login": "登錄", + "afterRegister": "註冊成功。 請等待管理員批准您的請求。", + "passwordNotMatch": "密碼不匹配。 確認密碼並重試。", + "invalidDetailsMessage": "請輸入有效的詳細信息。", + "talawaApiUnavailable": "Talawa-API 服務不可用。 請檢查您的網絡連接並稍等片刻。" + }, + "userNavbar": { + "talawa": "塔拉瓦", + "home": "家", + "people": "人們", + "events": "活動", + "chat": "聊天", + "donate": "捐", + "myTasks": "我的任務", + "settings": "設置", + "language": "語言", + "logout": "登出", + "close": "關閉" + }, + "userOrganizations": { + "allOrganizations": "所有組織", + "joinedOrganizations": "加入組織", + "createdOrganizations": "創建的組織", + "search": "搜索", + "nothingToShow": "這裡沒有什麼可展示的。" + }, + "userSidebar": { + "yourOrganizations": "您的組織", + "noOrganizations": "您還沒有加入任何組織。", + "viewAll": "查看全部" } } diff --git a/src/App.tsx b/src/App.tsx index 981aa19e03..0d125fe546 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,6 +6,7 @@ import * as installedPlugins from 'components/plugins/index'; import styles from './App.module.css'; import { CHECK_AUTH } from 'GraphQl/Queries/Queries'; import SecuredRoute from 'components/SecuredRoute/SecuredRoute'; +import SecuredRouteForUser from 'components/UserPortal/SecuredRouteForUser/SecuredRouteForUser'; import LoginPage from 'screens/LoginPage/LoginPage'; import OrganizationEvents from 'screens/OrganizationEvents/OrganizationEvents'; import OrganizationPeople from 'screens/OrganizationPeople/OrganizationPeople'; @@ -22,6 +23,10 @@ import Requests from 'screens/Requests/Requests'; import BlockUser from 'screens/BlockUser/BlockUser'; import MemberDetail from 'screens/MemberDetail/MemberDetail'; +// User Portal Components +import UserLoginPage from 'screens/UserPortal/UserLoginPage/UserLoginPage'; +import Organizations from 'screens/UserPortal/Organizations/Organizations'; + function app(): JSX.Element { /*const { updatePluginLinks, updateInstalled } = bindActionCreators( actionCreators, @@ -98,6 +103,14 @@ function app(): JSX.Element { {extraRoutes} + + {/* User Portal Routes */} + + + diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index efd7224e1d..4cc13c83f3 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -430,6 +430,54 @@ export const ORGANIZATION_POST_CONNECTION_LIST = gql` } } `; + +export const USER_ORGANIZATION_CONNECTION = gql` + query organizationsConnection($first: Int, $skip: Int, $filter: String) { + organizationsConnection( + first: $first + skip: $skip + where: { name_contains: $filter } + orderBy: name_ASC + ) { + _id + name + image + description + isPublic + creator { + firstName + lastName + } + } + } +`; + +export const USER_JOINED_ORGANIZATIONS = gql` + query UserJoinedOrganizations($id: ID!) { + users(where: { id: $id }) { + joinedOrganizations { + _id + name + description + image + } + } + } +`; + +export const USER_CREATED_ORGANIZATIONS = gql` + query UserJoinedOrganizations($id: ID!) { + users(where: { id: $id }) { + createdOrganizations { + _id + name + description + image + } + } + } +`; + /** * @name PLUGIN_GET * @description used to fetch list of plugins diff --git a/src/components/UserPortal/Login/Login.module.css b/src/components/UserPortal/Login/Login.module.css new file mode 100644 index 0000000000..98be9db02a --- /dev/null +++ b/src/components/UserPortal/Login/Login.module.css @@ -0,0 +1,29 @@ +.forgotPasswordContainer { + display: flex; + justify-content: flex-end; + flex-direction: row; + margin: 5px 0px; +} + +.forgotPasswordText { + color: black; + font-size: 12px; + margin: 2px 0px; +} + +.borderNone { + border: none; +} + +.colorWhite { + color: white; +} + +.colorPrimary { + background: #31bb6b; +} + +.colorPrimaryHover:hover { + background: #31bb6b; + border: none; +} diff --git a/src/components/UserPortal/Login/Login.test.tsx b/src/components/UserPortal/Login/Login.test.tsx new file mode 100644 index 0000000000..2ce9ebe9cc --- /dev/null +++ b/src/components/UserPortal/Login/Login.test.tsx @@ -0,0 +1,296 @@ +import type { SetStateAction } from 'react'; +import React from 'react'; +import { act, 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 { LOGIN_MUTATION } from 'GraphQl/Mutations/mutations'; +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import Login from './Login'; +import { toast } from 'react-toastify'; + +const MOCKS = [ + { + request: { + query: LOGIN_MUTATION, + variables: { + email: 'johndoe@gmail.com', + password: 'johndoe', + }, + }, + result: { + data: { + login: { + user: { + _id: '1', + userType: 'ADMIN', + adminApproved: true, + }, + accessToken: 'accessToken', + refreshToken: 'refreshToken', + }, + }, + }, + }, + { + request: { + query: LOGIN_MUTATION, + variables: { + email: 'johndoe@gmail.com', + password: 'jdoe', + }, + }, + result: { + data: { + login: { + user: { + _id: '1', + userType: 'ADMIN', + adminApproved: false, + }, + accessToken: 'accessToken', + refreshToken: 'refreshToken', + }, + }, + }, + }, + { + request: { + query: LOGIN_MUTATION, + variables: { + email: 'invalid@gmail.com', + password: 'anything', + }, + }, + result: {}, + }, +]; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +const setCurrentMode: React.Dispatch> = jest.fn(); + +const props = { + setCurrentMode, +}; + +describe('Testing Login Component [User Portal]', () => { + test('Component should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Expect the mode to be changed to Register', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('setRegisterBtn')); + + expect(setCurrentMode).toBeCalledWith('register'); + }); + + test('toast.error is triggered if the email input is empty.', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('loginBtn')); + + expect(toast.error).toBeCalledWith( + 'Please enter a valid email and password.' + ); + }); + + test('toast.error is triggered if the password input is empty.', async () => { + const formData = { + email: 'johndoe@gmail.com', + password: 'joe', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type( + screen.getByPlaceholderText(/Enter your email address/i), + formData.email + ); + userEvent.click(screen.getByTestId('loginBtn')); + + expect(toast.error).toBeCalledWith( + 'Please enter a valid email and password.' + ); + }); + + test('Incorrect password is entered.', async () => { + const formData = { + email: 'invalid@gmail.com', + password: 'anything', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type( + screen.getByPlaceholderText(/Enter your email address/i), + formData.email + ); + + userEvent.type( + screen.getByPlaceholderText(/Enter your password/i), + formData.password + ); + + userEvent.click(screen.getByTestId('loginBtn')); + + expect(toast.error).toBeCalled(); + + await wait(); + }); + + test('Login details are entered correctly.', async () => { + const formData = { + email: 'johndoe@gmail.com', + password: 'johndoe', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type( + screen.getByPlaceholderText(/Enter your email address/i), + formData.email + ); + + userEvent.type( + screen.getByPlaceholderText(/Enter your password/i), + formData.password + ); + + userEvent.click(screen.getByTestId('loginBtn')); + + await wait(); + }); + + test('Current user has not been approved by admin.', async () => { + const formData = { + email: 'johndoe@gmail.com', + password: 'jdoe', + }; + + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type( + screen.getByPlaceholderText(/Enter your email address/i), + formData.email + ); + + userEvent.type( + screen.getByPlaceholderText(/Enter your password/i), + formData.password + ); + + userEvent.click(screen.getByTestId('loginBtn')); + + expect(toast.error).toBeCalled(); + + await wait(); + }); +}); diff --git a/src/components/UserPortal/Login/Login.tsx b/src/components/UserPortal/Login/Login.tsx new file mode 100644 index 0000000000..d434434916 --- /dev/null +++ b/src/components/UserPortal/Login/Login.tsx @@ -0,0 +1,147 @@ +import type { ChangeEvent, SetStateAction } from 'react'; +import React from 'react'; +import { Button, Form, InputGroup } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; +import { LockOutlined } from '@mui/icons-material'; +import { Link } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; +import { toast } from 'react-toastify'; + +import { LOGIN_MUTATION } from 'GraphQl/Mutations/mutations'; +import styles from './Login.module.css'; +import { errorHandler } from 'utils/errorHandler'; + +interface InterfaceLoginProps { + setCurrentMode: React.Dispatch>; +} + +export default function login(props: InterfaceLoginProps): JSX.Element { + const { t } = useTranslation('translation', { keyPrefix: 'userLogin' }); + + const { setCurrentMode } = props; + + const handleModeChangeToRegister = (): void => { + setCurrentMode('register'); + }; + + const [loginMutation] = useMutation(LOGIN_MUTATION); + + const [loginVariables, setLoginVariables] = React.useState({ + email: '', + password: '', + }); + + const handleLogin = async (): Promise => { + if (!(loginVariables.email && loginVariables.password)) { + toast.error(t('invalidDetailsMessage')); + } else { + try { + const { data } = await loginMutation({ + variables: { + email: loginVariables.email, + password: loginVariables.password, + }, + }); + + if (data.login.user.adminApproved) { + localStorage.setItem('token', data.login.accessToken); + localStorage.setItem('userId', data.login.user._id); + + navigator.clipboard.writeText(''); + /* istanbul ignore next */ + window.location.assign('/user/organizations'); + } else { + toast.warn(t('notAuthorised')); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + } + }; + + /* istanbul ignore next */ + const handleEmailChange = (e: ChangeEvent): void => { + const email = e.target.value; + + setLoginVariables({ + email, + password: loginVariables.password, + }); + }; + + /* istanbul ignore next */ + const handlePasswordChange = (e: ChangeEvent): void => { + const password = e.target.value; + + setLoginVariables({ + email: loginVariables.email, + password, + }); + }; + + return ( + <> +

{t('login')}

+
{t('loginIntoYourAccount')}
+ +
+
{t('emailAddress')}
+ + + + + + +
{t('password')}
+ + + + + + +
+ +
+ + {t('forgotPassword')} + +
+ + +
+ + + ); +} diff --git a/src/components/UserPortal/OrganizationCard/OrganizationCard.module.css b/src/components/UserPortal/OrganizationCard/OrganizationCard.module.css new file mode 100644 index 0000000000..87eee09d9a --- /dev/null +++ b/src/components/UserPortal/OrganizationCard/OrganizationCard.module.css @@ -0,0 +1,17 @@ +.mainContainer { + width: 100%; + display: flex; + flex-direction: row; + padding: 10px; + cursor: pointer; + background-color: white; + border-radius: 10px; + box-shadow: 2px 2px 8px 0px #c8c8c8; + overflow: hidden; +} + +.organizationDetails { + display: flex; + flex-direction: column; + justify-content: center; +} diff --git a/src/components/UserPortal/OrganizationCard/OrganizationCard.test.tsx b/src/components/UserPortal/OrganizationCard/OrganizationCard.test.tsx new file mode 100644 index 0000000000..f04c87b526 --- /dev/null +++ b/src/components/UserPortal/OrganizationCard/OrganizationCard.test.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { I18nextProvider } from 'react-i18next'; + +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import OrganizationCard from './OrganizationCard'; + +const link = new StaticMockLink([], true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +let props = { + id: '1', + name: 'organizationName', + image: '', + description: 'organizationDescription', +}; + +describe('Testing Organization Card Component [User Portal]', () => { + test('Component should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Component should be rendered properly if organization Image is not undefined', async () => { + props = { + ...props, + image: 'organizationImage', + }; + + render( + + + + + + + + + + ); + + await wait(); + }); +}); diff --git a/src/components/UserPortal/OrganizationCard/OrganizationCard.tsx b/src/components/UserPortal/OrganizationCard/OrganizationCard.tsx new file mode 100644 index 0000000000..bfc9652d72 --- /dev/null +++ b/src/components/UserPortal/OrganizationCard/OrganizationCard.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import aboutImg from 'assets/images/defaultImg.png'; +import styles from './OrganizationCard.module.css'; + +interface InterfaceOrganizationCardProps { + id: string; + name: string; + image: string; + description: string; +} + +function organizationCard(props: InterfaceOrganizationCardProps): JSX.Element { + const imageUrl = props.image ? props.image : aboutImg; + + return ( +
+ +
+ {props.name} + {props.description} +
+
+ ); +} + +export default organizationCard; diff --git a/src/components/UserPortal/Register/Register.module.css b/src/components/UserPortal/Register/Register.module.css new file mode 100644 index 0000000000..1fc2a34af2 --- /dev/null +++ b/src/components/UserPortal/Register/Register.module.css @@ -0,0 +1,15 @@ +.loginText { + cursor: pointer; +} + +.borderNone { + border: none; +} + +.colorWhite { + color: white; +} + +.colorPrimary { + background: #31bb6b; +} diff --git a/src/components/UserPortal/Register/Register.test.tsx b/src/components/UserPortal/Register/Register.test.tsx new file mode 100644 index 0000000000..f730baca73 --- /dev/null +++ b/src/components/UserPortal/Register/Register.test.tsx @@ -0,0 +1,268 @@ +import type { SetStateAction } from 'react'; +import React from 'react'; +import { act, 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-dom'; +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'; + +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', + }, + }, + }, + }, +]; + +const formData = { + firstName: 'John', + lastName: 'Doe', + email: 'johndoe@gmail.com', + password: 'johnDoe', + confirmPassword: 'johnDoe', +}; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +const setCurrentMode: React.Dispatch> = jest.fn(); + +const props = { + setCurrentMode, +}; + +describe('Testing Register Component [User Portal]', () => { + test('Component should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Expect the mode to be changed to Login', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('setLoginBtn')); + + expect(setCurrentMode).toBeCalledWith('login'); + }); + + test('Expect toast.error to be called if email input is empty', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('registerBtn')); + + expect(toast.error).toBeCalledWith('Please enter valid details.'); + }); + + test('Expect toast.error to be called if password input is empty', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId('emailInput'), formData.email); + userEvent.click(screen.getByTestId('registerBtn')); + + expect(toast.error).toBeCalledWith('Please enter valid details.'); + }); + + test('Expect toast.error to be called if first name input is empty', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId('passwordInput'), formData.password); + + userEvent.type(screen.getByTestId('emailInput'), formData.email); + + userEvent.click(screen.getByTestId('registerBtn')); + + expect(toast.error).toBeCalledWith('Please enter valid details.'); + }); + + test('Expect toast.error to be called if last name input is empty', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId('passwordInput'), formData.password); + + userEvent.type(screen.getByTestId('emailInput'), formData.email); + + userEvent.type(screen.getByTestId('firstNameInput'), formData.firstName); + + userEvent.click(screen.getByTestId('registerBtn')); + + expect(toast.error).toBeCalledWith('Please enter valid details.'); + }); + + test("Expect toast.error to be called if confirmPassword doesn't match with password", async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId('passwordInput'), formData.password); + + userEvent.type(screen.getByTestId('emailInput'), formData.email); + + userEvent.type(screen.getByTestId('firstNameInput'), formData.firstName); + + userEvent.type(screen.getByTestId('lastNameInput'), formData.lastName); + + userEvent.click(screen.getByTestId('registerBtn')); + + expect(toast.error).toBeCalledWith( + "Password doesn't match. Confirm Password and try again." + ); + }); + + test('Expect toast.success to be called if valid credentials are entered.', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId('passwordInput'), formData.password); + + userEvent.type( + screen.getByTestId('confirmPasswordInput'), + formData.confirmPassword + ); + + userEvent.type(screen.getByTestId('emailInput'), formData.email); + + userEvent.type(screen.getByTestId('firstNameInput'), formData.firstName); + + userEvent.type(screen.getByTestId('lastNameInput'), formData.lastName); + + userEvent.click(screen.getByTestId('registerBtn')); + + await wait(); + + expect(toast.success).toBeCalledWith( + 'Successfully registered. Please wait for admin to approve your request.' + ); + }); +}); diff --git a/src/components/UserPortal/Register/Register.tsx b/src/components/UserPortal/Register/Register.tsx new file mode 100644 index 0000000000..1867eceecc --- /dev/null +++ b/src/components/UserPortal/Register/Register.tsx @@ -0,0 +1,221 @@ +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 { + setCurrentMode: React.Dispatch>; +} + +export default function register(props: InterfaceRegisterProps): JSX.Element { + const { setCurrentMode } = props; + + const { t } = useTranslation('translation', { keyPrefix: 'userRegister' }); + + const handleModeChangeToLogin = (): void => { + setCurrentMode('login'); + }; + + const [registerMutation] = useMutation(SIGNUP_MUTATION); + + const [registerVariables, setRegisterVariables] = React.useState({ + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + }); + + const handleRegister = async (): Promise => { + if ( + !( + registerVariables.email && + registerVariables.password && + registerVariables.firstName && + registerVariables.lastName + ) + ) { + toast.error(t('invalidDetailsMessage')); + } else if ( + registerVariables.password !== registerVariables.confirmPassword + ) { + toast.error(t('passwordNotMatch')); + } else { + try { + await registerMutation({ + variables: { + firstName: registerVariables.firstName, + lastName: registerVariables.lastName, + email: registerVariables.email, + password: registerVariables.password, + }, + }); + + toast.success(t('afterRegister')); + + /* istanbul ignore next */ + setRegisterVariables({ + firstName: '', + lastName: '', + email: '', + password: '', + confirmPassword: '', + }); + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + } + }; + + /* istanbul ignore next */ + const handleFirstName = (e: ChangeEvent): void => { + const firstName = e.target.value; + + setRegisterVariables({ ...registerVariables, firstName }); + }; + + /* istanbul ignore next */ + const handleLastName = (e: ChangeEvent): void => { + const lastName = e.target.value; + + setRegisterVariables({ ...registerVariables, lastName }); + }; + + /* istanbul ignore next */ + const handleEmailChange = (e: ChangeEvent): void => { + const email = e.target.value; + + setRegisterVariables({ ...registerVariables, email }); + }; + + /* istanbul ignore next */ + const handlePasswordChange = (e: ChangeEvent): void => { + const password = e.target.value; + + setRegisterVariables({ ...registerVariables, password }); + }; + + /* istanbul ignore next */ + const handleConfirmPasswordChange = ( + e: ChangeEvent + ): void => { + const confirmPassword = e.target.value; + + setRegisterVariables({ ...registerVariables, confirmPassword }); + }; + + return ( + <> +

{t('register')}

+
+
{t('firstName')}
+ + + + + + +
{t('lastName')}
+ + + + + + +
{t('emailAddress')}
+ + + + + + +
{t('password')}
+ + + + + + +
{t('confirmPassword')}
+ + + + + + +
+ + +
+ {t('alreadyhaveAnAccount')}{' '} + + {t('login')} + +
+ + ); +} diff --git a/src/components/UserPortal/SecuredRouteForUser/SecuredRouteForUser.tsx b/src/components/UserPortal/SecuredRouteForUser/SecuredRouteForUser.tsx new file mode 100644 index 0000000000..967e6c00a0 --- /dev/null +++ b/src/components/UserPortal/SecuredRouteForUser/SecuredRouteForUser.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Redirect, Route } from 'react-router-dom'; + +const SecuredRouteForUser = (props: any): JSX.Element => { + const isLoggedIn = localStorage.getItem('IsLoggedIn'); + return isLoggedIn === 'TRUE' ? ( + <> + + + ) : ( + + ); +}; + +export default SecuredRouteForUser; diff --git a/src/components/UserPortal/UserNavbar/UserNavbar.module.css b/src/components/UserPortal/UserNavbar/UserNavbar.module.css new file mode 100644 index 0000000000..30eaa52187 --- /dev/null +++ b/src/components/UserPortal/UserNavbar/UserNavbar.module.css @@ -0,0 +1,21 @@ +.talawaImage { + width: 40px; + height: auto; + margin-top: -5px; + border: 2px solid white; + margin-right: 10px; + background-color: white; + border-radius: 10px; +} + +.boxShadow { + box-shadow: 4px 4px 8px 4px #c8c8c8; +} + +.colorWhite { + color: white; +} + +.colorPrimary { + background: #31bb6b; +} diff --git a/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx b/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx new file mode 100644 index 0000000000..ed51680185 --- /dev/null +++ b/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx @@ -0,0 +1,167 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import { act, render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import cookies from 'js-cookie'; +import { StaticMockLink } from 'utils/StaticMockLink'; + +import UserNavbar from './UserNavbar'; +import userEvent from '@testing-library/user-event'; + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +const link = new StaticMockLink([], true); + +describe('Testing UserNavbar Component [User Portal]', () => { + afterEach(async () => { + await act(async () => { + await i18nForTest.changeLanguage('en'); + }); + }); + + test('Component should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('The language is switched to English', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('languageIcon')); + + userEvent.click(screen.getByTestId('changeLanguageBtn0')); + + await wait(); + + expect(cookies.get('i18next')).toBe('en'); + }); + + test('The language is switched to fr', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('languageIcon')); + + userEvent.click(screen.getByTestId('changeLanguageBtn1')); + + await wait(); + + expect(cookies.get('i18next')).toBe('fr'); + }); + + test('The language is switched to hi', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('languageIcon')); + + userEvent.click(screen.getByTestId('changeLanguageBtn2')); + + await wait(); + + expect(cookies.get('i18next')).toBe('hi'); + }); + + test('The language is switched to sp', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('languageIcon')); + + userEvent.click(screen.getByTestId('changeLanguageBtn3')); + + await wait(); + + expect(cookies.get('i18next')).toBe('sp'); + }); + + test('The language is switched to zh', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('languageIcon')); + + userEvent.click(screen.getByTestId('changeLanguageBtn4')); + + await wait(); + + expect(cookies.get('i18next')).toBe('zh'); + }); +}); diff --git a/src/components/UserPortal/UserNavbar/UserNavbar.tsx b/src/components/UserPortal/UserNavbar/UserNavbar.tsx new file mode 100644 index 0000000000..6e3f83cc5c --- /dev/null +++ b/src/components/UserPortal/UserNavbar/UserNavbar.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import styles from './UserNavbar.module.css'; +import TalawaImage from 'assets/images/talawa-logo-200x200.png'; +import { Container, Dropdown, Navbar } from 'react-bootstrap'; +import { languages } from 'utils/languages'; +import i18next from 'i18next'; +import cookies from 'js-cookie'; +import PermIdentityIcon from '@mui/icons-material/PermIdentity'; +import LanguageIcon from '@mui/icons-material/Language'; +import { useTranslation } from 'react-i18next'; + +function userNavbar(): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'userNavbar', + }); + + const [currentLanguageCode, setCurrentLanguageCode] = React.useState( + /* istanbul ignore next */ + cookies.get('i18next') || 'en' + ); + + /* istanbul ignore next */ + const handleLogout = (): void => { + localStorage.clear(); + window.location.replace('/user'); + }; + + const userName = localStorage.getItem('name'); + + return ( + + + + Talawa Branding + {t('talawa')} + + + + + + + + + + {languages.map((language, index: number) => ( + => { + setCurrentLanguageCode(language.code); + await i18next.changeLanguage(language.code); + }} + disabled={currentLanguageCode === language.code} + data-testid={`changeLanguageBtn${index}`} + > + {' '} + {language.name} + + ))} + + + + + + + + + + {userName} + + {t('settings')} + {t('myTasks')} + + {t('logout')} + + + + + + + ); +} + +export default userNavbar; diff --git a/src/components/UserPortal/UserSidebar/UserSidebar.module.css b/src/components/UserPortal/UserSidebar/UserSidebar.module.css new file mode 100644 index 0000000000..f635b0b05f --- /dev/null +++ b/src/components/UserPortal/UserSidebar/UserSidebar.module.css @@ -0,0 +1,65 @@ +.mainContainer { + display: flex; + overflow: hidden; + flex-direction: column; + align-items: center; + padding: 0px 10px; + padding-top: 50px; + flex-grow: 1; +} + +@media screen and (max-width: 700px) { + .mainContainer { + display: none; + } +} + +.userDetails { + display: flex; + flex-direction: column; + align-items: center; + padding-top: 20px; +} + +.boxShadow { + box-shadow: 4px 4px 8px 4px #c8c8c8; +} + +.organizationsConatiner { + width: 100%; + padding-top: 50px; +} + +.heading { + text-align: center; + padding: 10px 0px; +} + +.orgName { + font-size: 16px; + font-weight: 600; + margin-top: 4px; +} + +.alignRight { + width: 100%; + text-align: right; + padding: 5px; +} + +.link { + text-decoration: none !important; + color: black; +} + +.rounded { + border-radius: 10px !important; +} + +.colorLight { + background-color: #f5f5f5; +} + +.marginTop { + margin-top: -2px; +} diff --git a/src/components/UserPortal/UserSidebar/UserSidebar.test.tsx b/src/components/UserPortal/UserSidebar/UserSidebar.test.tsx new file mode 100644 index 0000000000..1c3e5928c3 --- /dev/null +++ b/src/components/UserPortal/UserSidebar/UserSidebar.test.tsx @@ -0,0 +1,211 @@ +import React from 'react'; +import { act, render } from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { I18nextProvider } from 'react-i18next'; + +import { + USER_DETAILS, + USER_JOINED_ORGANIZATIONS, +} from 'GraphQl/Queries/Queries'; +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import UserSidebar from './UserSidebar'; + +const MOCKS = [ + { + request: { + query: USER_DETAILS, + variables: { + id: localStorage.getItem('userId'), + }, + }, + result: { + data: { + user: { + __typename: 'User', + image: null, + firstName: 'Noble', + lastName: 'Mittal', + email: 'noble@mittal.com', + role: 'SUPERADMIN', + appLanguageCode: 'en', + userType: 'SUPERADMIN', + pluginCreationAllowed: true, + adminApproved: true, + createdAt: '2023-02-18T09:22:27.969Z', + adminFor: [], + createdOrganizations: [], + joinedOrganizations: [], + organizationUserBelongsTo: null, + organizationsBlockedBy: [], + createdEvents: [], + registeredEvents: [], + eventAdmin: [], + membershipRequests: [], + }, + }, + }, + }, + { + request: { + query: USER_DETAILS, + variables: { + id: '2', + }, + }, + result: { + data: { + user: { + __typename: 'User', + image: 'adssda', + firstName: 'Noble', + lastName: 'Mittal', + email: 'noble@mittal.com', + role: 'SUPERADMIN', + appLanguageCode: 'en', + userType: 'SUPERADMIN', + pluginCreationAllowed: true, + adminApproved: true, + createdAt: '2023-02-18T09:22:27.969Z', + adminFor: [], + createdOrganizations: [], + joinedOrganizations: [], + organizationUserBelongsTo: null, + organizationsBlockedBy: [], + createdEvents: [], + registeredEvents: [], + eventAdmin: [], + membershipRequests: [], + }, + }, + }, + }, + { + request: { + query: USER_JOINED_ORGANIZATIONS, + variables: { + id: localStorage.getItem('userId'), + }, + }, + result: { + data: { + users: [ + { + joinedOrganizations: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af2', + name: 'Any Organization', + image: '', + description: 'New Desc', + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: USER_JOINED_ORGANIZATIONS, + variables: { + id: '2', + }, + }, + result: { + data: { + users: [ + { + joinedOrganizations: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af2', + name: 'Any Organization', + image: 'dadsa', + description: 'New Desc', + }, + ], + }, + ], + }, + }, + }, +]; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +describe('Testing UserSidebar Component [User Portal]', () => { + test('Component should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Component should be rendered properly when userImage is not undefined', async () => { + const beforeUserId = localStorage.getItem('userId'); + + localStorage.setItem('userId', '2'); + + render( + + + + + + + + + + ); + + await wait(); + if (beforeUserId) { + localStorage.setItem('userId', beforeUserId); + } + }); + + test('Component should be rendered properly when organizationImage is not undefined', async () => { + const beforeUserId = localStorage.getItem('userId'); + + localStorage.setItem('userId', '2'); + + render( + + + + + + + + + + ); + + await wait(); + + if (beforeUserId) { + localStorage.setItem('userId', beforeUserId); + } + }); +}); diff --git a/src/components/UserPortal/UserSidebar/UserSidebar.tsx b/src/components/UserPortal/UserSidebar/UserSidebar.tsx new file mode 100644 index 0000000000..69771ce63f --- /dev/null +++ b/src/components/UserPortal/UserSidebar/UserSidebar.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import AboutImg from 'assets/images/defaultImg.png'; +import styles from './UserSidebar.module.css'; +import { ListGroup } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; +import ChevronRightIcon from '@mui/icons-material/ChevronRight'; +import { useQuery } from '@apollo/client'; +import { + USER_DETAILS, + USER_JOINED_ORGANIZATIONS, +} from 'GraphQl/Queries/Queries'; +import { useTranslation } from 'react-i18next'; + +function userSidebar(): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'userSidebar', + }); + + const [organizations, setOrganizations] = React.useState([]); + const [details, setDetails] = React.useState({} as any); + + const userId: string | null = localStorage.getItem('userId'); + + const { data } = useQuery(USER_JOINED_ORGANIZATIONS, { + variables: { id: userId }, + }); + + const { data: data2 } = useQuery(USER_DETAILS, { + variables: { id: userId }, + }); + + /* istanbul ignore next */ + React.useEffect(() => { + if (data) { + setOrganizations(data.users[0].joinedOrganizations); + } + }, [data]); + + /* istanbul ignore next */ + React.useEffect(() => { + if (data2) { + setDetails(data2.user); + } + }, [data2]); + + return ( +
+ +
+
+ {`${details.firstName} ${details.lastName}`} +
+
{details.email}
+
+
+
+ {t('yourOrganizations')} +
+ + {organizations.length ? ( + organizations.map((organization: any, index) => { + return ( + +
+ +
{organization.name}
+
+
+ ); + }) + ) : ( +
{t('noOrganizations')}
+ )} +
+
+ + {t('viewAll')} + + +
+
+
+ ); +} + +export default userSidebar; diff --git a/src/screens/UserPortal/Organizations/Organizations.module.css b/src/screens/UserPortal/Organizations/Organizations.module.css new file mode 100644 index 0000000000..6f83c82d8a --- /dev/null +++ b/src/screens/UserPortal/Organizations/Organizations.module.css @@ -0,0 +1,44 @@ +.borderNone { + border: none; +} + +.colorWhite { + color: white; +} + +.maxWidth { + max-width: 300px; +} + +.colorLight { + background-color: #f5f5f5; +} + +.mainContainer { + width: 50%; + flex-grow: 3; + padding: 40px; + max-height: 100%; + overflow: auto; +} + +.content { + height: fit-content; + min-height: calc(100% - 40px); +} + +.gap { + gap: 20px; +} + +.paddingY { + padding: 30px 0px; +} + +.containerHeight { + height: calc(100vh - 61px); +} + +.colorPrimary { + background: #31bb6b; +} diff --git a/src/screens/UserPortal/Organizations/Organizations.test.tsx b/src/screens/UserPortal/Organizations/Organizations.test.tsx new file mode 100644 index 0000000000..6d92831940 --- /dev/null +++ b/src/screens/UserPortal/Organizations/Organizations.test.tsx @@ -0,0 +1,221 @@ +import React from 'react'; +import { act, render, screen } from '@testing-library/react'; +import { MockedProvider } from '@apollo/react-testing'; +import { I18nextProvider } from 'react-i18next'; + +import { + USER_CREATED_ORGANIZATIONS, + USER_JOINED_ORGANIZATIONS, + USER_ORGANIZATION_CONNECTION, +} from 'GraphQl/Queries/Queries'; +import { BrowserRouter } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import Organizations from './Organizations'; +import userEvent from '@testing-library/user-event'; + +const MOCKS = [ + { + request: { + query: USER_CREATED_ORGANIZATIONS, + variables: { + id: localStorage.getItem('userId'), + }, + }, + result: { + data: { + users: [ + { + createdOrganizations: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af2', + name: 'createdOrganization', + image: '', + description: 'New Desc', + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: USER_ORGANIZATION_CONNECTION, + variables: { + filter: '', + }, + }, + result: { + data: { + organizationsConnection: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af2', + image: '', + name: 'anyOrganization1', + description: 'desc', + isPublic: true, + creator: { __typename: 'User', firstName: 'John', lastName: 'Doe' }, + }, + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af3', + image: '', + name: 'anyOrganization2', + description: 'desc', + isPublic: true, + creator: { __typename: 'User', firstName: 'John', lastName: 'Doe' }, + }, + ], + }, + }, + }, + { + request: { + query: USER_JOINED_ORGANIZATIONS, + variables: { + id: localStorage.getItem('userId'), + }, + }, + result: { + data: { + users: [ + { + joinedOrganizations: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af2', + name: 'joinedOrganization', + image: '', + description: 'New Desc', + }, + ], + }, + ], + }, + }, + }, + { + request: { + query: USER_ORGANIZATION_CONNECTION, + variables: { + filter: '2', + }, + }, + result: { + data: { + organizationsConnection: [ + { + __typename: 'Organization', + _id: '6401ff65ce8e8406b8f07af3', + image: '', + name: 'anyOrganization2', + description: 'desc', + isPublic: true, + creator: { __typename: 'User', firstName: 'John', lastName: 'Doe' }, + }, + ], + }, + }, + }, +]; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +describe('Testing Organizations Screen [User Portal]', () => { + test('Screen should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Search works properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.type(screen.getByTestId('searchInput'), '2'); + await wait(); + + expect(screen.queryByText('anyOrganization2')).toBeInTheDocument(); + expect(screen.queryByText('anyOrganization1')).not.toBeInTheDocument(); + }); + + test('Mode is changed to joined organizations', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('modeChangeBtn')); + await wait(); + userEvent.click(screen.getByTestId('modeBtn1')); + await wait(); + + expect(screen.queryAllByText('joinedOrganization')).not.toBe([]); + }); + + test('Mode is changed to created organizations', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('modeChangeBtn')); + await wait(); + userEvent.click(screen.getByTestId('modeBtn2')); + await wait(); + + expect(screen.queryAllByText('createdOrganization')).not.toBe([]); + }); +}); diff --git a/src/screens/UserPortal/Organizations/Organizations.tsx b/src/screens/UserPortal/Organizations/Organizations.tsx new file mode 100644 index 0000000000..5d1402a94d --- /dev/null +++ b/src/screens/UserPortal/Organizations/Organizations.tsx @@ -0,0 +1,206 @@ +import React from 'react'; +import UserNavbar from 'components/UserPortal/UserNavbar/UserNavbar'; +import OrganizationCard from 'components/UserPortal/OrganizationCard/OrganizationCard'; +import UserSidebar from 'components/UserPortal/UserSidebar/UserSidebar'; +import { Dropdown, Form, InputGroup } from 'react-bootstrap'; +import PaginationList from 'components/PaginationList/PaginationList'; +import { + USER_CREATED_ORGANIZATIONS, + USER_JOINED_ORGANIZATIONS, + USER_ORGANIZATION_CONNECTION, +} from 'GraphQl/Queries/Queries'; +import { useQuery } from '@apollo/client'; +import { SearchOutlined } from '@mui/icons-material'; +import styles from './Organizations.module.css'; +import { useTranslation } from 'react-i18next'; + +interface InterfaceOrganizationCardProps { + id: string; + name: string; + image: string; + description: string; +} + +export default function organizations(): JSX.Element { + const { t } = useTranslation('translation', { + keyPrefix: 'userOrganizations', + }); + + const [page, setPage] = React.useState(0); + const [rowsPerPage, setRowsPerPage] = React.useState(5); + const [organizations, setOrganizations] = React.useState([]); + const [filterName, setFilterName] = React.useState(''); + const [mode, setMode] = React.useState(0); + + const modes = [ + t('allOrganizations'), + t('joinedOrganizations'), + t('createdOrganizations'), + ]; + + const userId: string | null = localStorage.getItem('userId'); + + const { data, refetch } = useQuery(USER_ORGANIZATION_CONNECTION, { + variables: { filter: filterName }, + }); + + const { data: data2 } = useQuery(USER_JOINED_ORGANIZATIONS, { + variables: { id: userId }, + }); + + const { data: data3 } = useQuery(USER_CREATED_ORGANIZATIONS, { + variables: { id: userId }, + }); + + /* istanbul ignore next */ + const handleChangePage = ( + _event: React.MouseEvent | null, + newPage: number + ): void => { + setPage(newPage); + }; + + /* istanbul ignore next */ + const handleChangeRowsPerPage = ( + event: React.ChangeEvent + ): void => { + const newRowsPerPage = event.target.value; + + setRowsPerPage(parseInt(newRowsPerPage, 10)); + setPage(0); + }; + + const handleSearch = ( + event: React.ChangeEvent + ): void => { + const newFilter = event.target.value; + setFilterName(newFilter); + + const filter = { + filter: newFilter, + }; + + refetch(filter); + }; + + /* istanbul ignore next */ + React.useEffect(() => { + if (data) { + setOrganizations(data.organizationsConnection); + } + }, [data]); + + /* istanbul ignore next */ + React.useEffect(() => { + if (mode == 0) { + if (data) { + setOrganizations(data.organizationsConnection); + } + } else if (mode == 1) { + if (data2) { + setOrganizations(data2.users[0].joinedOrganizations); + } + } else if (mode == 2) { + if (data3) { + setOrganizations(data3.users[0].createdOrganizations); + } + } + }, [mode]); + + return ( + <> + +
+ +
+
+ + + + + + + + + {modes[mode]} + + + {modes.map((value, index) => { + return ( + setMode(index)} + > + {value} + + ); + })} + + +
+
+
+ {organizations && organizations.length > 0 ? ( + (rowsPerPage > 0 + ? organizations.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ) + : /* istanbul ignore next */ + organizations + ).map((organization: any, index) => { + const cardProps: InterfaceOrganizationCardProps = { + name: organization.name, + image: organization.image, + id: organization._id, + description: organization.description, + }; + return ; + }) + ) : ( + {t('nothingToShow')} + )} +
+ + + + + + +
+
+
+
+ + ); +} diff --git a/src/screens/UserPortal/UserLoginPage/UserLoginPage.module.css b/src/screens/UserPortal/UserLoginPage/UserLoginPage.module.css new file mode 100644 index 0000000000..fcb12ce679 --- /dev/null +++ b/src/screens/UserPortal/UserLoginPage/UserLoginPage.module.css @@ -0,0 +1,48 @@ +body::before { + content: none !important; +} + +.leftPane { + align-items: center; + width: 60%; + min-width: 300px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.palisadoesImage { + width: 100%; + height: auto; + max-width: 700px; +} + +.talawaImage { + width: 40%; + height: auto; + margin-left: 50%; + transform: translateX(-50%); +} + +.mainContainer { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + min-height: 100vh; +} + +.contentContainer { + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: center; + padding: 20px 50px; + background-color: #f5f5f5; +} + +@media only screen and (max-width: 800px) { + .leftPane { + width: 100%; + } +} diff --git a/src/screens/UserPortal/UserLoginPage/UserLoginPage.test.tsx b/src/screens/UserPortal/UserLoginPage/UserLoginPage.test.tsx new file mode 100644 index 0000000000..ff2175ccc1 --- /dev/null +++ b/src/screens/UserPortal/UserLoginPage/UserLoginPage.test.tsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import { act, render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { store } from 'state/store'; +import i18nForTest from 'utils/i18nForTest'; +import cookies from 'js-cookie'; +import { StaticMockLink } from 'utils/StaticMockLink'; + +import UserLoginPage from './UserLoginPage'; +import userEvent from '@testing-library/user-event'; + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} + +const link = new StaticMockLink([], true); + +describe('Testing User Login Page Screen [User Portal]', () => { + afterEach(async () => { + await act(async () => { + await i18nForTest.changeLanguage('en'); + }); + }); + + test('Screen should be rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + }); + + test('Expect the defualt language to be en', async () => { + cookies.remove('i18next'); + render( + + + + + + + + + + ); + + await wait(); + + expect(screen.getByText(/English/i)).toBeInTheDocument(); + }); + + test('Expect the language to be changed to fr', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByText('English')); + + userEvent.click(screen.getByTestId('changeLanguageBtn1')); + + await wait(); + + expect(cookies.get('i18next')).toBe('fr'); + }); + + test('Expect the language to be changed to hi', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByText('English')); + + userEvent.click(screen.getByTestId('changeLanguageBtn2')); + + await wait(); + + expect(cookies.get('i18next')).toBe('hi'); + }); + + test('Expect the language to be changed to sp', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByText('English')); + + userEvent.click(screen.getByTestId('changeLanguageBtn3')); + + await wait(); + + expect(cookies.get('i18next')).toBe('sp'); + }); + + test('Expect the language to be changed to zh', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByText('English')); + + userEvent.click(screen.getByTestId('changeLanguageBtn4')); + + await wait(); + + expect(cookies.get('i18next')).toBe('zh'); + }); +}); diff --git a/src/screens/UserPortal/UserLoginPage/UserLoginPage.tsx b/src/screens/UserPortal/UserLoginPage/UserLoginPage.tsx new file mode 100644 index 0000000000..dae86340bb --- /dev/null +++ b/src/screens/UserPortal/UserLoginPage/UserLoginPage.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { Dropdown, DropdownButton } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import cookies from 'js-cookie'; +import { languages } from 'utils/languages'; +import i18next from 'i18next'; + +import styles from './UserLoginPage.module.css'; +import PalisadoesImage from 'assets/images/palisadoes_logo.png'; +import TalawaImage from 'assets/images/talawa-logo-200x200.png'; +import Login from 'components/UserPortal/Login/Login'; +import Register from 'components/UserPortal/Register/Register'; + +export default function userLoginPage(): JSX.Element { + const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); + + const currentLanguageCode = cookies.get('i18next') || 'en'; + + const currentLanguage = languages.find( + (language) => language.code === currentLanguageCode + )?.name; + + const [currentMode, setCurrentMode] = React.useState('login'); + + const loginRegisterProps = { + setCurrentMode: setCurrentMode, + }; + + return ( +
+
+ Palisadoes Branding +
+

{t('fromPalisadoes')}

+
+
+
+ + {languages.map((language, index: number) => ( + => { + await i18next.changeLanguage(language.code); + }} + disabled={currentLanguageCode === language.code} + data-testid={`changeLanguageBtn${index}`} + > + {' '} + {language.name} + + ))} + + Talawa Branding + { + /* istanbul ignore next */ + currentMode === 'login' ? ( + + ) : ( + + ) + } +
+
+ ); +}