diff --git a/package-lock.json b/package-lock.json index c43fb82c4f..f11ca8eb3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,8 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.8.3", "@mui/material": "^5.14.1", + "@mui/private-theming": "^5.14.13", + "@mui/system": "^5.14.12", "@mui/x-charts": "^6.0.0-alpha.13", "@mui/x-data-grid": "^6.8.0", "@mui/x-date-pickers": "^6.6.0", @@ -57,6 +59,7 @@ "react-dom": "^17.0.2", "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.1", + "react-infinite-scroll-component": "^6.1.0", "react-redux": "^7.2.5", "react-router-dom": "^5.2.0", "react-scripts": "5.0.1", @@ -2190,9 +2193,9 @@ "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" }, "node_modules/@babel/runtime": { - "version": "7.23.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz", - "integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", + "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -3470,12 +3473,12 @@ } }, "node_modules/@mui/private-theming": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.11.tgz", - "integrity": "sha512-MSnNNzTu9pfKLCKs1ZAKwOTgE4bz+fQA0fNr8Jm7NDmuWmw0CaN9Vq2/MHsatE7+S0A25IAKby46Uv1u53rKVQ==", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.15.tgz", + "integrity": "sha512-V2Xh+Tu6A07NoSpup0P9m29GwvNMYl5DegsGWqlOTJyAV7cuuVjmVPqxgvL8xBng4R85xqIQJRMjtYYktoPNuQ==", "dependencies": { - "@babel/runtime": "^7.22.15", - "@mui/utils": "^5.14.11", + "@babel/runtime": "^7.23.2", + "@mui/utils": "^5.14.15", "prop-types": "^15.8.1" }, "engines": { @@ -3496,11 +3499,11 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.11.tgz", - "integrity": "sha512-jdUlqRgTYQ8RMtPX4MbRZqar6W2OiIb6J5KEFbIu4FqvPrk44Each4ppg/LAqp1qNlBYq5i+7Q10MYLMpDxX9A==", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.15.tgz", + "integrity": "sha512-mbOjRf867BysNpexe5Z/P8s3bWzDPNowmKhi7gtNDP/LPEeqAfiDSuC4WPTXmtvse1dCl30Nl755OLUYuoi7Mw==", "dependencies": { - "@babel/runtime": "^7.22.15", + "@babel/runtime": "^7.23.2", "@emotion/cache": "^11.11.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3527,15 +3530,15 @@ } }, "node_modules/@mui/system": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.11.tgz", - "integrity": "sha512-yl8xV+y0k7j6dzBsHabKwoShmjqLa8kTxrhUI3JpqLG358VRVMJRW/ES0HhvfcCi4IVXde+Tc2P3K1akGL8zoA==", - "dependencies": { - "@babel/runtime": "^7.22.15", - "@mui/private-theming": "^5.14.11", - "@mui/styled-engine": "^5.14.11", - "@mui/types": "^7.2.4", - "@mui/utils": "^5.14.11", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.15.tgz", + "integrity": "sha512-zr0Gdk1RgKiEk+tCMB900LaOpEC8NaGvxtkmMdL/CXgkqQZSVZOt2PQsxJWaw7kE4YVkIe4VukFVc43qcq9u3w==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/private-theming": "^5.14.15", + "@mui/styled-engine": "^5.14.15", + "@mui/types": "^7.2.7", + "@mui/utils": "^5.14.15", "clsx": "^2.0.0", "csstype": "^3.1.2", "prop-types": "^15.8.1" @@ -3574,11 +3577,11 @@ } }, "node_modules/@mui/types": { - "version": "7.2.4", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", - "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==", + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.7.tgz", + "integrity": "sha512-sofpWmcBqOlTzRbr1cLQuUDKaUYVZTw8ENQrtL39TECRNENEzwgnNPh6WMfqMZlMvf1Aj9DLg74XPjnLr0izUQ==", "peerDependencies": { - "@types/react": "*" + "@types/react": "^17.0.0 || ^18.0.0" }, "peerDependenciesMeta": { "@types/react": { @@ -3587,12 +3590,12 @@ } }, "node_modules/@mui/utils": { - "version": "5.14.11", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.11.tgz", - "integrity": "sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==", + "version": "5.14.15", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.15.tgz", + "integrity": "sha512-QBfHovAvTa0J1jXuYDaXGk+Yyp7+Fm8GSqx6nK2JbezGqzCFfirNdop/+bL9Flh/OQ/64PeXcW4HGDdOge+n3A==", "dependencies": { - "@babel/runtime": "^7.22.15", - "@types/prop-types": "^15.7.5", + "@babel/runtime": "^7.23.2", + "@types/prop-types": "^15.7.8", "prop-types": "^15.8.1", "react-is": "^18.2.0" }, @@ -5081,9 +5084,9 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" }, "node_modules/@types/prop-types": { - "version": "15.7.5", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", - "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==" + "version": "15.7.9", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.9.tgz", + "integrity": "sha512-n1yyPsugYNSmHgxDFjicaI2+gCNjsBck8UX9kuofAKlc0h1bL+20oSF72KeNaW2DUlesbEVCFgyV2dPGTiY42g==" }, "node_modules/@types/q": { "version": "1.5.5", @@ -20018,6 +20021,17 @@ } } }, + "node_modules/react-infinite-scroll-component": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-infinite-scroll-component/-/react-infinite-scroll-component-6.1.0.tgz", + "integrity": "sha512-SQu5nCqy8DxQWpnUVLx7V7b7LcA37aM7tvoWjTLZp1dk6EJibM5/4EJKzOnl07/BsM1Y40sKLuqjCwwH/xV0TQ==", + "dependencies": { + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -23386,6 +23400,14 @@ "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.2.tgz", "integrity": "sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==" }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", diff --git a/package.json b/package.json index cc1e769e3a..1fbc68abf3 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "react-dom": "^17.0.2", "react-google-recaptcha": "^2.1.0", "react-i18next": "^11.18.1", + "react-infinite-scroll-component": "^6.1.0", "react-redux": "^7.2.5", "react-router-dom": "^5.2.0", "react-scripts": "5.0.1", diff --git a/public/locales/en.json b/public/locales/en.json index 37d239a2e8..6f3d16b201 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -86,7 +86,7 @@ "cancel": "Cancel", "noOrgErrorTitle": "Organizations Not Found", "noOrgErrorDescription": "Please create an organization through dashboard", - + "endOfResults": "End of results", "manageFeatures": "Manage Features", "manageFeaturesInfo": "Creation Successful ! Please select features that you want to enale for this organization from the plugin store.", "goToStore": "Go to Plugin Store", @@ -109,6 +109,15 @@ "name": "Name", "email": "Email", "roles_userType": "Role/User-Type", + "joined_organizations": "Joined Organizations", + "blocked_organizations": "Blocked Organizations", + "orgJoinedBy": "Organizations Joined By", + "orgThatBlocked": "Organizations That Blocked", + "endOfResults": "End of results", + "hasNotJoinedAnyOrg": "has not joined any organization", + "isNotBlockedByAnyOrg": "is not blocked by any organization", + "searchByOrgName": "Search By Organization Name", + "view": "View", "admin": "ADMIN", "superAdmin": "SUPERADMIN", "user": "USER", @@ -131,6 +140,7 @@ "accept": "Accept", "reject": "Reject", "enterName": "Enter Name", + "endOfResults": "End of results", "loadingRequests": "Loading Requests...", "noRequestFound": "No Request Found", "sort": "Sort", @@ -153,6 +163,13 @@ "events": "Events", "blockedUsers": "Blocked Users", "requests": "Requests", + "viewAll": "View All", + "upcomingEvents": "Upcoming Events", + "noUpcomingEvents": "No Upcoming Events", + "latestPosts": "Latest Posts", + "noPostsPresent": "No Posts Present", + "membershipRequests": "Membership requests", + "noMembershipRequests": "No Membership requests present", "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." }, "organizationPeople": { @@ -251,17 +268,25 @@ "posts": "Posts", "createPost": "Create Post", "postDetails": "Post Details", + "postTitle1": "Write title of the post", "postTitle": "Title", "information": "Information", + "information1": "Write information of the post", "image": "Post Image", - "video": "Video", + "video": "Post Video", "addPost": "Add Post", "searchTitle": "Search By Title", "searchText": "Search By Text", "ptitle": "Post Title", "postDes": "What do you to talk about?", "Title": "Title", - "Text": "Text" + "Text": "Text", + "cancel": "Cancel", + "searchBy": "Search By", + "Oldest": "Oldest First", + "Latest": "Latest First", + "sortPost": "Sort Post", + "tag": " Your browser does not support the video tag" }, "postNotFound": { "post": "Post", @@ -283,6 +308,7 @@ "author": "Author", "imageURL": "Image URL", "videoURL": "Video URL", + "edit": "Edit Post", "deletePost": "Delete Post", "deletePostMsg": "Do you want to remove this post?", "no": "No", @@ -296,6 +322,8 @@ "updatePost": "Update Post", "postDeleted": "Post deleted successfully.", "postUpdated": "Post Updated successfully.", + "tag": " Your browser does not support the video tag", + "pin": "Pin Post", "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." }, "blockUnblockUser": { @@ -328,7 +356,7 @@ "enterNewPassword": "Enter New Password", "cofirmNewPassword": "Confirm New Password", "changePassword": "Change Password", - "backToHome": "Back to Home", + "backToLogin": "Back to Login", "userOtp": "e.g. 12345", "password": "Password", "emailNotRegistered": "Email is not registered.", @@ -377,9 +405,9 @@ "deleteOrg": { "deleteOrganization": "Delete Organization", "deleteMsg": "Do you want to delete this organization?", - "no": "No", - "yes": "Yes", - "longDelOrgMsg": "By clicking on Delete organization button you will the organization will be permanently deleted along with its events, tags and all related data." + "cancel": "Cancel", + "confirmDelete": "Confirm Delete", + "longDelOrgMsg": "By clicking on Delete Organization button the organization will be permanently deleted along with its events, tags and all related data." }, "userUpdate": { "firstName": "First Name", diff --git a/public/locales/fr.json b/public/locales/fr.json index 0d483a607f..1386bd8d96 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -84,6 +84,7 @@ "sort": "Trier", "filter": "Filtre", "cancel": "Annuler", + "endOfResults": "Fin des résultats", "noOrgErrorTitle": "Organisations non trouvées", "noOrgErrorDescription": "Veuillez créer une organisation via le tableau de bord", "noResultsFoundFor": "Aucun résultat trouvé pour " @@ -104,6 +105,15 @@ "name": "Nom", "email": "E-mail", "roles_userType": "Rôle/Type d'utilisateur", + "joined_organizations": "Organisations rejointes", + "blocked_organizations": "Organisations bloquées", + "endOfResults": "Fin des résultats", + "orgJoinedBy": "Organisation rejointe par", + "orgThatBlocked": "Organisation bloquée par", + "hasNotJoinedAnyOrg": "n'a rejoint aucune organisation", + "isNotBlockedByAnyOrg": "n'est bloqué par aucune organisation", + "searchByOrgName": "Rechercher par nom d'organisation", + "view": "Vue", "admin": "ADMIN", "superAdmin": "SUPERADMIN", "user": "UTILISATEUR", @@ -125,6 +135,7 @@ "accept": "Accepter", "reject": "Rejeter", "enterName": "Entrez le nom", + "endOfResults": "Fin des résultats", "loadingRequests": "Chargement des demandes...", "noRequestFound": "Aucune demande trouvée", "sort": "Trier", @@ -146,6 +157,13 @@ "events": "Événements", "blockedUsers": "Utilisateurs bloqués", "requests": "Demandes", + "viewAll": "Voir tout", + "upcomingEvents": "Événements à venir", + "noUpcomingEvents": "Aucun événement à venir", + "latestPosts": "Derniers messages", + "noPostsPresent": "Aucune publication présente", + "membershipRequests": "Demandes d'adhésion", + "noMembershipRequests": "Aucune demande d'adhésion", "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." }, "organizationPeople": { @@ -239,22 +257,30 @@ "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." }, "orgPost": { - "title": "Postes de Talawa", - "searchPost": "Rechercher un article", - "posts": "Des postes", - "createPostnot found": "Créer un article", + "title": "Talawa Publications", + "searchPost": "Rechercher une publication", + "posts": "Publications", + "createPost": "Créer une publication", "postDetails": "Détails de la publication", + "postTitle1": "Écrire le titre de la publication", "postTitle": "Titre", "information": "Informations", - "image": "Image", - "video": "Vidéo", - "addPost": "Ajouter un article", + "information1": "Écrire les informations de la publication", + "image": "Image de la publication", + "video": "Vidéo de la publication", + "addPost": "Ajouter une publication", "searchTitle": "Rechercher par titre", "searchText": "Rechercher par texte", - "ptitle": "Titre de l'article", - "postDes": "De quoi parlez-vous ?", + "ptitle": "Titre de la publication", + "postDes": "De quoi voulez-vous parler?", "Title": "Titre", - "Text": "Texte" + "Text": "Texte", + "cancel": "Annuler", + "searchBy": "Rechercher par", + "Oldest": "Les plus anciennes d'abord", + "Latest": "Les plus récentes d'abord", + "sortPost": "Trier les publications", + "tag": "Votre navigateur ne prend pas en charge la balise vidéo" }, "postNotFound": { "post": "Poste", @@ -276,19 +302,22 @@ "author": "Auteur", "imageURL": "URL de l'image", "videoURL": "URL de la vidéo", - "deletePost": "Supprimer le message", - "deletePostMsg": "Voulez-vous supprimer ce message ?", + "edit": "Modifier la publication", + "deletePost": "Supprimer la publication", + "deletePostMsg": "Voulez-vous supprimer cette publication ?", "no": "Non", "yes": "Oui", - "editPost": "Modifier le message", + "editPost": "Modifier la publication", "postTitle": "Titre", "information": "Informations", "image": "Image", "video": "Vidéo", - "close": "Proche", - "updatePost": "Mettre à jour le message", - "postDeleted": "Message supprimé avec succès.", - "postUpdated": "Message mis à jour avec succès.", + "close": "Fermer", + "pin": "Épingler la publication", + "updatePost": "Mettre à jour la publication", + "postDeleted": "Publication supprimée avec succès.", + "postUpdated": "Publication mise à jour avec succès.", + "tag": "Votre navigateur ne prend pas en charge la balise vidéo", "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." }, "blockUnblockUser": { @@ -321,7 +350,7 @@ "enterNewPassword": "Entrez un nouveau mot de passe", "cofirmNewPassword": "Confirmer le nouveau mot de passe", "changePassword": "Changer le mot de passe", - "backToHome": "De retour à la maison", + "backToLogin": "Retour à la connexion", "userOtp": "par exemple. 12345", "password": "Mot de passe", "emailNotRegistered": "L'e-mail n'est pas enregistré.", @@ -372,9 +401,9 @@ "deleteOrg": { "deleteOrganization": "Supprimer l'organisation", "deleteMsg": "Voulez-vous supprimer cette organisation ?", - "no": "Non", - "yes": "Oui", - "longDelOrgMsg": "En cliquant sur le bouton Supprimer l'organisation, l'organisation sera définitivement supprimée, ainsi que ses événements, étiquettes et toutes les données associées." + "cancel": "Annuler", + "confirmDelete": "Confirmer la suppression", + "longDelOrgMsg": "En cliquant sur le bouton Supprimer l'organisation, l'organisation sera définitivement supprimée ainsi que ses événements, balises et toutes les données associées." }, "userUpdate": { "firstName": "Prénom", diff --git a/public/locales/hi.json b/public/locales/hi.json index e5fc2584ff..f7091382ab 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -84,6 +84,7 @@ "sort": "छांटें", "filter": "फ़िल्टर", "cancel": "रद्द करना", + "endOfResults": "परिणामों का अंत", "noOrgErrorTitle": "संगठन नहीं मिला", "noOrgErrorDescription": "कृपया डैशबोर्ड के माध्यम से एक संगठन बनाएं", "noResultsFoundFor": "के लिए कोई परिणाम नहीं मिला " @@ -104,6 +105,15 @@ "name": "नाम", "email": "ईमेल", "roles_userType": "भूमिका/उपयोगकर्ता-प्रकार", + "joined_organizations": "संगठनों में शामिल हुए", + "blocked_organizations": "अवरोधित संगठन", + "endOfResults": "परिणामों का अंत", + "orgJoinedBy": "द्वारा शामिल हुए संगठन", + "orgThatBlocked": "जिन संगठनों ने अवरोधित किया", + "hasNotJoinedAnyOrg": "किसी भी संगठन में शामिल नहीं है", + "isNotBlockedByAnyOrg": "किसी भी संगठन द्वारा अवरोधित नहीं है", + "searchByOrgName": "संगठन के नाम से खोजें", + "view": "देखें", "admin": "व्यवस्थापक", "superAdmin": "सुपरएडमिन", "user": "उपयोगकर्ता", @@ -125,6 +135,7 @@ "accept": "स्वीकार करना", "reject": "अस्वीकार", "enterName": "नाम दर्ज करें", + "endOfResults": "परिणामों का अंत", "loadingRequests": "अनुरोध लोड हो रहा है ...", "noRequestFound": "कोई अनुरोध नहीं मिला।", "sort": "छांटें", @@ -146,6 +157,13 @@ "events": "आयोजन", "blockedUsers": "रोके गए उपयोगकर्ता", "requests": "अनुरोध", + "viewAll": "सभी देखें", + "upcomingEvents": "आगामी घटनाएँ", + "noUpcomingEvents": "कोई आगामी घटनाएँ नहीं", + "latestPosts": "नवीनतम पोस्ट", + "noPostsPresent": "कोई पोस्ट नहीं है", + "membershipRequests": "सदस्यता अनुरोध", + "noMembershipRequests": "कोई सदस्यता अनुरोध नहीं", "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" }, "organizationPeople": { @@ -239,22 +257,30 @@ "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" }, "orgPost": { - "title": "तलावा पोस्ट", - "searchPost": "खोज पोस्ट", - "posts": "पोस्ट", - "createPost": "पोस्ट बनाएं", + "title": "तलवा पोस्ट्स", + "searchPost": "पोस्ट खोजें", + "posts": "पोस्ट्स", + "createPost": "पोस्ट बनाएँ", "postDetails": "पोस्ट विवरण", + "postTitle1": "पोस्ट का शीर्षक लिखें", "postTitle": "शीर्षक", "information": "जानकारी", - "image": "छवि", - "video": "वीडियो", + "information1": "पोस्ट की जानकारी लिखें", + "image": "पोस्ट छवि", + "video": "पोस्ट वीडियो", "addPost": "पोस्ट जोड़ें", "searchTitle": "शीर्षक से खोजें", - "searchText": "पाठ द्वारा खोजें", - "ptitle": "शीर्षक पोस्ट करें", - "postDes": "आपको किस बारे में बात करनी है?", + "searchText": "टेक्स्ट से खोजें", + "ptitle": "पोस्ट का शीर्षक", + "postDes": "आप किस बारे में बात करना चाहते हैं?", "Title": "शीर्षक", - "Text": "मूलपाठ" + "Text": "टेक्स्ट", + "cancel": "रद्द करें", + "searchBy": "इसके द्वारा खोजें", + "Oldest": "सबसे पुराना पहले", + "Latest": "सबसे नवीनतम पहले", + "sortPost": "पोस्ट को क्रमित करें", + "tag": "आपका ब्राउज़र वीडियो टैग का समर्थन नहीं करता" }, "postNotFound": { "post": "पोस्ट", @@ -274,22 +300,25 @@ }, "orgPostCard": { "author": "लेखक", - "imageURL": "छवि यूआरएल", - "videoURL": "वीडियो यूआरएल", - "deletePost": "पोस्ट को हटाएं", + "imageURL": "छवि URL", + "videoURL": "वीडियो URL", + "edit": "पोस्ट संपादित करें", + "deletePost": "पोस्ट हटाएं", "deletePostMsg": "क्या आप इस पोस्ट को हटाना चाहते हैं?", "no": "नहीं", "yes": "हाँ", - "editPost": "संपादित पोस्ट", + "editPost": "पोस्ट संपादित करें", "postTitle": "शीर्षक", "information": "जानकारी", "image": "छवि", "video": "वीडियो", - "close": "बंद करना", - "updatePost": "अपडेट पोस्ट", - "postDeleted": "पोस्ट सफलतापूर्वक हटाई गई।", - "postUpdated": "पोस्ट सफलतापूर्वक अपडेट किया गया।", - "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" + "close": "बंद करें", + "updatePost": "पोस्ट अपडेट करें", + "postDeleted": "पोस्ट सफलतापूर्वक हटा दी गई है।", + "pin": "पोस्ट पिन करें", + "postUpdated": "पोस्ट सफलतापूर्वक अपडेट की गई है।", + "tag": "आपका ब्राउज़र वीडियो टैग का समर्थन नहीं करता", + "talawaApiUnavailable": "Talawa-API सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जाँचें।" }, "blockUnblockUser": { "title": "तलावा ब्लॉक/अनब्लॉक यूजर", @@ -321,7 +350,7 @@ "enterNewPassword": "नया पासवर्ड दर्ज करें", "cofirmNewPassword": "नए पासवर्ड की पुष्टि करें", "changePassword": "पासवर्ड बदलें", - "backToHome": "घर वापिस जा रहा हूँ", + "backToLogin": "लॉगिन पर वापस जाएं", "userOtp": "उदाहरण के लिए 12345", "password": "पासवर्ड", "emailNotRegistered": "ईमेल पंजीकृत नहीं है।", @@ -372,9 +401,9 @@ "deleteOrg": { "deleteOrganization": "संगठन हटाएं", "deleteMsg": "क्या आप इस संगठन को हटाना चाहते हैं?", - "no": "नहीं", - "yes": "हां", - "longDelOrgMsg": "संगठन हटाने के बटन पर क्लिक करके, संगठन को स्थायित रूप से हटा दिया जाएगा, साथ ही उसके आयोजन, टैग और सभी संबंधित डेटा भी हटा दिया जाएगा।" + "cancel": "रद्द करना", + "confirmDelete": "हटाने की पुष्टि करें", + "longDelOrgMsg": "संगठन को हमेशा के लिए हटा देने के लिए संगठन हटाने के बटन पर क्लिक करके, उसके इवेंट्स, टैग्स और सभी संबंधित डेटा सहित सभी जानकारी हटा दी जाएगी।" }, "userUpdate": { "firstName": "पहला नाम", diff --git a/public/locales/sp.json b/public/locales/sp.json index b65d83558d..dd53814d12 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -84,6 +84,7 @@ "sort": "Ordenar", "filter": "Filtrar", "cancel": "Cancelar", + "endOfResults": "Fin de los resultados", "noOrgErrorTitle": "Organizaciones no encontradas", "noOrgErrorDescription": "Por favor, crea una organización a través del panel de control", "noResultsFoundFor": "No se encontraron resultados para " @@ -104,6 +105,15 @@ "name": "Nombre", "email": "Correo electrónico", "roles_userType": "Rol/Tipo de usuario", + "joined_organizations": "Organizaciones unidas", + "blocked_organizations": "Organizaciones bloqueadas", + "endOfResults": "Fin de los resultados", + "orgJoinedBy": "Organizaciones unidas por", + "orgThatBlocked": "Organizaciones bloqueadas por", + "hasNotJoinedAnyOrg": "No se ha unido a ninguna organización.", + "isNotBlockedByAnyOrg": "No está bloqueado por ninguna organización.", + "searchByOrgName": "Buscar por nombre de organización", + "view": "Ver", "admin": "ADMINISTRACIÓN", "superAdmin": "SUPERADMIN", "user": "USUARIO", @@ -125,6 +135,7 @@ "accept": "Aceptar", "reject": "Rechazar", "enterName": "Ingrese su nombre", + "endOfResults": "Fin de los resultados", "loadingRequests": "Cargando solicitudes ...", "noRequestFound": "No se encontró ninguna solicitud.", "sort": "Ordenar", @@ -146,6 +157,13 @@ "events": "Eventos", "blockedUsers": "Usuarios bloqueados", "requests": "Solicitudes", + "viewAll": "Ver Todo", + "upcomingEvents": "Próximos Eventos", + "noUpcomingEvents": "No Hay Próximos Eventos", + "latestPosts": "Últimas Publicaciones", + "noPostsPresent": "No Hay Publicaciones Presentes", + "membershipRequests": "Solicitudes de Membresía", + "noMembershipRequests": "No Hay Solicitudes de Membresía", "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." }, "organizationPeople": { @@ -239,22 +257,30 @@ "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." }, "orgPost": { - "title": "Puestos Talawa", - "searchPost": "Buscar publicación", + "title": "Publicaciones de Talawa", + "searchPost": "Buscar Publicación", "posts": "Publicaciones", - "createPost": "Crear publicación", - "postDetails": "Detalles de la publicación", + "createPost": "Crear Publicación", + "postDetails": "Detalles de la Publicación", + "postTitle1": "Escribir título de la publicación", "postTitle": "Título", "information": "Información", - "image": "Imagen", - "video": "Video", - "addPost": "Añadir publicación", - "searchTitle": "Buscar por título", - "searchText": "Buscar por texto", - "ptitle": "Título de la entrada", - "postDes": "¿De qué hablar?", + "information1": "Escribir información de la publicación", + "image": "Imagen de la Publicación", + "video": "Video de la Publicación", + "addPost": "Agregar Publicación", + "searchTitle": "Buscar por Título", + "searchText": "Buscar por Texto", + "ptitle": "Título de la Publicación", + "postDes": "¿De qué quieres hablar?", "Title": "Título", - "Text": "Texto" + "Text": "Texto", + "cancel": "Cancelar", + "searchBy": "Buscar por", + "Oldest": "Más Antiguas Primero", + "Latest": "Más Recientes Primero", + "sortPost": "Ordenar Publicaciones", + "tag": "Su navegador no admite la etiqueta de video" }, "postNotFound": { "post": "Publicaciones", @@ -272,24 +298,27 @@ "admin not found!": "Administrador no encontrado!", "roles not found!": "roles no encontrados!" }, + "orgPostCard": { "author": "Autor", - "imageURL": "URL de la imagen", - "videoURL": "URL del vídeo", - "deletePost": "Eliminar mensaje", - "deletePostMsg": "¿Quieres eliminar esta publicación?", + "imageURL": "URL de la Imagen", + "videoURL": "URL del Video", + "edit": "Editar Publicación", + "deletePost": "Eliminar Publicación", + "deletePostMsg": "¿Desea eliminar esta publicación?", "no": "No", "yes": "Sí", - "editPost": "Editar post", + "editPost": "Editar Publicación", "postTitle": "Título", "information": "Información", "image": "Imagen", "video": "Video", - "close": "Cerca", - "updatePost": "Actualizar publicación", - "postDeleted": "Publicación eliminada con éxito.", - "postUpdated": "Publicación actualizada con éxito.", - "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." + "close": "Cerrar", + "updatePost": "Actualizar Publicación", + "postDeleted": "Publicación eliminada exitosamente.", + "postUpdated": "Publicación actualizada exitosamente.", + "tag": "Su navegador no admite la etiqueta de video", + "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está en funcionamiento? Compruebe también su conectividad de red." }, "blockUnblockUser": { "title": "Usuario de bloqueo/desbloqueo de Talawa", @@ -321,7 +350,7 @@ "enterNewPassword": "Ingrese nueva clave", "cofirmNewPassword": "Confirmar nueva contraseña", "changePassword": "Cambia la contraseña", - "backToHome": "De vuelta a casa", + "backToLogin": "Volver al inicio de sesión", "userOtp": "por ejemplo 12345", "password": "Contraseña", "emailNotRegistered": "El correo electrónico no está registrado.", @@ -372,9 +401,9 @@ "deleteOrg": { "deleteOrganization": "Eliminar organización", "deleteMsg": "¿Desea eliminar esta organización?", - "no": "No", - "yes": "Sí", - "longDelOrgMsg": "Al hacer clic en el botón de Eliminar organización, se eliminará permanentemente la organización junto con sus eventos, etiquetas y todos los datos relacionados." + "cancel": "Cancelar", + "confirmDelete": "Confirmar eliminación", + "longDelOrgMsg": "Al hacer clic en el botón Eliminar organización, la organización se eliminará permanentemente junto con sus eventos, etiquetas y todos los datos relacionados." }, "userUpdate": { "firstName": "Primer nombre", diff --git a/public/locales/zh.json b/public/locales/zh.json index 1a95a15dd0..dbb937c94e 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -84,6 +84,7 @@ "sort": "排序", "filter": "過濾", "cancel": "取消", + "endOfResults": "結果結束", "noOrgErrorTitle": "找不到组织", "noOrgErrorDescription": "请通过仪表板创建一个组织", "noResultsFoundFor": "未找到结果 " @@ -104,6 +105,15 @@ "name": "姓名", "email": "電子郵件", "roles_userType": "角色/用戶類型", + "joined_organizations": "加入的組織", + "blocked_organizations": "阻止的組織", + "endOfResults": "結果結束", + "orgJoinedBy": "加入組織", + "orgThatBlocked": "阻止組織", + "hasNotJoinedAnyOrg": "尚未加入任何組織", + "isNotBlockedByAnyOrg": "沒有被任何組織阻止", + "searchByOrgName": "按組織名稱搜索", + "view": "視圖", "admin": "行政", "superAdmin": "超級管理員", "user": "用戶", @@ -125,6 +135,7 @@ "accept": "接受", "reject": "拒絕", "enterName": "输入名字", + "endOfResults": "結果結束", "loadingRequests": "正在加載請求...", "noRequestFound": "找不到請求。", "sort": "排序", @@ -146,6 +157,13 @@ "events": "事件", "blockedUsers": "被阻止的用戶", "requests": "请求", + "viewAll": "查看全部", + "upcomingEvents": "即将举办的活动", + "noUpcomingEvents": "没有即将举办的活动", + "latestPosts": "最新帖子", + "noPostsPresent": "没有帖子", + "membershipRequests": "会员申请", + "noMembershipRequests": "没有会员申请", "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" }, "organizationPeople": { @@ -239,22 +257,30 @@ "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" }, "orgPost": { - "title": "塔拉瓦郵報", + "title": "塔拉瓦帖子", "searchPost": "搜索帖子", "posts": "帖子", - "createPost": "創建帖子", - "postDetails": "發布詳細信息", - "postTitle": "標題", + "createPost": "创建帖子", + "postDetails": "帖子详情", + "postTitle1": "填写帖子标题", + "postTitle": "标题", "information": "信息", - "image": "圖片", - "video": "視頻", + "information1": "填写帖子信息", + "image": "帖子图片", + "video": "帖子视频", "addPost": "添加帖子", - "searchTitle": "按標題搜索", + "searchTitle": "按标题搜索", "searchText": "按文本搜索", - "ptitle": "帖子標題", - "postDes": "你要談什麼?", - "Title": "標題", - "Text": "文本" + "ptitle": "帖子标题", + "postDes": "您想讨论什么?", + "Title": "标题", + "Text": "文本", + "cancel": "取消", + "searchBy": "按方式搜索", + "Oldest": "最旧的优先", + "Latest": "最新的优先", + "sortPost": "排序帖子", + "tag": "您的浏览器不支持视频标签" }, "postNotFound": { "post": "郵政", @@ -274,22 +300,25 @@ }, "orgPostCard": { "author": "作者", - "imageURL": "圖片網址", - "videoURL": "視頻網址", - "deletePost": "刪除帖子", - "deletePostMsg": "你想刪除這個帖子嗎?", - "no": "不", - "yes": "是的", - "editPost": "編輯帖子", - "postTitle": "標題", + "imageURL": "图像链接", + "videoURL": "视频链接", + "edit": "编辑帖子", + "deletePost": "删除帖子", + "deletePostMsg": "您确定要删除此帖子吗?", + "no": "否", + "yes": "是", + "editPost": "编辑帖子", + "postTitle": "标题", "information": "信息", - "image": "圖片", - "video": "視頻", - "close": "關", + "image": "图片", + "video": "视频", + "close": "关闭", + "pin": "置顶帖子", "updatePost": "更新帖子", - "postDeleted": "帖子刪除成功。", - "postUpdated": "帖子更新成功。", - "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" + "postDeleted": "帖子成功删除。", + "postUpdated": "帖子成功更新。", + "tag": "您的浏览器不支持视频标签", + "talawaApiUnavailable": "Talawa-API 服务不可用。是否正在运行?还请检查您的网络连接。" }, "blockUnblockUser": { "title": "塔拉瓦封鎖/解除封鎖用戶", @@ -321,7 +350,7 @@ "enterNewPassword": "輸入新密碼", "cofirmNewPassword": "確認新密碼", "changePassword": "更改密碼", - "backToHome": "回到家", + "backToLogin": "回到登錄", "userOtp": "舉個例子。 12345", "password": "密碼", "emailNotRegistered": "電子郵件未註冊。", @@ -372,9 +401,9 @@ "deleteOrg": { "deleteOrganization": "删除组织", "deleteMsg": "您是否要删除此组织?", - "no": "否", - "yes": "是", - "longDelOrgMsg": "点击删除组织按钮后,将永久删除该组织以及其活动、标签和所有相关数据。" + "cancel": "取消", + "confirmDelete": "确认删除", + "longDelOrgMsg": "点击删除组织按钮,组织将被永久删除,包括其事件、标签和所有相关数据。" }, "userUpdate": { "firstName": "名", diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index 9444bdcd48..fb303cb840 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -402,8 +402,22 @@ export const ADD_PLUGIN_MUTATION = gql` `; export const UPDATE_POST_MUTATION = gql` - mutation UpdatePost($id: ID!, $title: String, $text: String) { - updatePost(id: $id, data: { title: $title, text: $text }) { + mutation UpdatePost( + $id: ID! + $title: String + $text: String + $imageUrl: String + $videoUrl: String + ) { + updatePost( + id: $id + data: { + title: $title + text: $text + imageUrl: $imageUrl + videoUrl: $videoUrl + } + ) { _id } } @@ -623,6 +637,23 @@ export const UNLIKE_COMMENT = gql` } `; +// Changes the role of a user in an organization +export const UPDATE_USER_ROLE_IN_ORG_MUTATION = gql` + mutation updateUserRoleInOrganization( + $organizationId: ID! + $userId: ID! + $role: String! + ) { + updateUserRoleInOrganization( + organizationId: $organizationId + userId: $userId + role: $role + ) { + _id + } + } +`; + export const CREATE_DIRECT_CHAT = gql` mutation createDirectChat($userIds: [ID!]!, $organizationId: ID!) { createDirectChat( @@ -644,3 +675,10 @@ export const PLUGIN_SUBSCRIPTION = gql` } } `; +export const TOGGLE_PINNED_POST = gql` + mutation TogglePostPin($id: ID!) { + togglePostPin(id: $id) { + _id + } + } +`; diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 95dfff3eb2..fccad7d88e 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -42,8 +42,12 @@ export const ORGANIZATION_LIST = gql` // Query to take the Organization list with filter option export const ORGANIZATION_CONNECTION_LIST = gql` - query OrganizationsConnection($filter: String) { - organizationsConnection(where: { name_contains: $filter }) { + query OrganizationsConnection($filter: String, $first: Int, $skip: Int) { + organizationsConnection( + where: { name_contains: $filter } + first: $first + skip: $skip + ) { _id image creator { @@ -64,14 +68,20 @@ export const ORGANIZATION_CONNECTION_LIST = gql` `; // Query to take the User list - export const USER_LIST = gql` - query Users($firstName_contains: String, $lastName_contains: String) { + query Users( + $firstName_contains: String + $lastName_contains: String + $skip: Int + $first: Int + ) { users( where: { firstName_contains: $firstName_contains lastName_contains: $lastName_contains } + skip: $skip + first: $first ) { firstName lastName @@ -80,13 +90,70 @@ export const USER_LIST = gql` email userType adminApproved + adminFor { + _id + } + createdAt organizationsBlockedBy { _id name + image + location + createdAt + creator { + _id + firstName + lastName + image + email + createdAt + } } joinedOrganizations { _id + name + image + location + createdAt + creator { + _id + firstName + lastName + image + email + createdAt + } } + } + } +`; + +export const USER_LIST_REQUEST = gql` + query Users( + $firstName_contains: String + $lastName_contains: String + $first: Int + $skip: Int + $userType: String + $adminApproved: Boolean + ) { + users( + where: { + firstName_contains: $firstName_contains + lastName_contains: $lastName_contains + } + skip: $skip + first: $first + userType: $userType + adminApproved: $adminApproved + ) { + firstName + lastName + image + _id + email + userType + adminApproved createdAt } } @@ -527,6 +594,7 @@ export const ORGANIZATION_POST_CONNECTION_LIST = gql` lastName email } + pinned likeCount commentCount comments { @@ -543,6 +611,7 @@ export const ORGANIZATION_POST_CONNECTION_LIST = gql` } text } + createdAt likedBy { _id firstName diff --git a/src/assets/svgs/key.svg b/src/assets/svgs/key.svg new file mode 100644 index 0000000000..a1f47615e8 --- /dev/null +++ b/src/assets/svgs/key.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/DeleteOrg/DeleteOrg.tsx b/src/components/DeleteOrg/DeleteOrg.tsx index e6442d6558..91fe4263f4 100644 --- a/src/components/DeleteOrg/DeleteOrg.tsx +++ b/src/components/DeleteOrg/DeleteOrg.tsx @@ -66,18 +66,18 @@ function deleteOrg(): JSX.Element { {t('deleteMsg')} diff --git a/src/components/EventCalendar/EventCalendar.tsx b/src/components/EventCalendar/EventCalendar.tsx index 336efd9882..4cb027647d 100644 --- a/src/components/EventCalendar/EventCalendar.tsx +++ b/src/components/EventCalendar/EventCalendar.tsx @@ -116,7 +116,7 @@ const Calendar: React.FC = ({ useEffect(() => { const data = filterData(eventData, orgData, userRole, userId); setEvents(data); - }, []); + }, [eventData, orgData, userRole, userId]); const handlePrevMonth = (): void => { if (currentMonth === 0) { diff --git a/src/components/LandingPage/LandingPage.module.css b/src/components/LandingPage/LandingPage.module.css deleted file mode 100644 index c912b5f41d..0000000000 --- a/src/components/LandingPage/LandingPage.module.css +++ /dev/null @@ -1,26 +0,0 @@ -.maintitle { - font-size: 18px; - color: #707070; - width: 70%; - margin: 35px auto 10px; - text-align: center; -} -.maintitle > span { - color: black; -} -.fromtitle { - font-size: 14px; - color: #707070; - margin: 40px auto 10px; - text-align: center; -} -.carouseldiv { - z-index: 1; - margin: 35px auto 10px; - align-items: center; - width: 80%; -} - -.carouseldiv img { - border-radius: 5px; -} diff --git a/src/components/LandingPage/LandingPage.test.tsx b/src/components/LandingPage/LandingPage.test.tsx deleted file mode 100644 index b4e798bab7..0000000000 --- a/src/components/LandingPage/LandingPage.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { Suspense } from 'react'; -import { render, waitFor } from '@testing-library/react'; -import { I18nextProvider } from 'react-i18next'; - -import LandingPage from './LandingPage'; -import type { NormalizedCacheObject } from '@apollo/client'; -import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client'; -import i18n from 'utils/i18n'; -import { BACKEND_URL } from 'Constant/constant'; - -const client: ApolloClient = new ApolloClient({ - cache: new InMemoryCache(), - uri: BACKEND_URL, -}); - -describe('Testing LandingPage', () => { - const fallbackLoader =
; - - test('should render props and text elements test for the page component', async () => { - render( - - - - - - - - ); - - await waitFor(() => { - const elements = document.querySelectorAll('*'); - const searchText = 'loginPage.fromPalisadoes'; - - for (let i = 0; i < elements.length; i++) { - const element = elements[i] as HTMLElement; - const text = element.innerText; - - if (text && text.includes(searchText)) { - break; - } - } - }); - }); -}); diff --git a/src/components/LandingPage/LandingPage.tsx b/src/components/LandingPage/LandingPage.tsx deleted file mode 100644 index fadb028dc9..0000000000 --- a/src/components/LandingPage/LandingPage.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import styles from './LandingPage.module.css'; -import slide1 from 'assets/images/palisadoes_logo.png'; -import { useTranslation } from 'react-i18next'; - -function landingPage(): JSX.Element { - const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); - - return ( - <> -
-
- First slide -
-

-

{t('fromPalisadoes')}

-

-
- - ); -} -export {}; -export default landingPage; diff --git a/src/components/LeftDrawer/LeftDrawer.tsx b/src/components/LeftDrawer/LeftDrawer.tsx index b5244b2df7..57eacce0a7 100644 --- a/src/components/LeftDrawer/LeftDrawer.tsx +++ b/src/components/LeftDrawer/LeftDrawer.tsx @@ -139,7 +139,7 @@ const leftDrawer = ({ }} >
- {userImage && userImage ? ( + {userImage && userImage !== 'null' ? ( {`profile ) : (
- {userImage && userImage ? ( + {userImage && userImage !== 'null' ? ( {`profile ) : ( h3 { font-size: 17px; } +.card { + width: 100%; + height: 20rem; + margin-bottom: 2rem; +} .postimage { - border-radius: 6px; + border-radius: 0px; + width: 100%; + height: 12rem; + max-width: 100%; + max-height: 12rem; + object-fit: cover; + position: relative; + color: black; +} +.preview { + display: flex; + position: relative; + width: 100%; + margin-top: 10px; + justify-content: center; +} +.preview img { + width: 400px; + height: auto; +} +.preview video { + width: 400px; + height: auto; +} +.nopostimage { + border-radius: 0px; + width: 100%; + height: 12rem; + max-height: 12rem; + object-fit: cover; + position: relative; +} +.cards:hover { + filter: brightness(0.8); +} +.cards:hover::before { + opacity: 0.5; +} +.knowMoreText { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + opacity: 0; + color: white; + padding: 10px; + font-weight: bold; + font-size: 1.5rem; + transition: opacity 0.3s ease-in-out; +} + +.cards:hover .knowMoreText { + opacity: 1; +} +.modal { + position: fixed; + top: 0; + left: 0; width: 100%; - max-width: 240px; - height: 150px; - max-height: 250px; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba( + 0, + 0, + 0, + 0.9 + ); /* Dark grey modal background with transparency */ + z-index: 9999; +} + +.modalContent { + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + padding: 20px; + max-width: 800px; + max-height: 600px; + overflow: auto; +} + +.modalImage { + flex: 1; + margin-right: 20px; + width: 25rem; + height: 15rem; +} +.nomodalImage { + flex: 1; + margin-right: 20px; + width: 100%; + height: 15rem; +} + +.modalImage img, +.modalImage video { + border-radius: 0px; + width: 100%; + height: 25rem; + max-width: 25rem; + max-height: 15rem; + object-fit: cover; + position: relative; +} +.modalInfo { + flex: 1; +} +.title { + font-size: 16px; + color: #000; + font-weight: 600; +} +.text { + font-size: 13px; + color: #000; + font-weight: 300; +} +.author { + color: #737373; + font-weight: 100; + font-size: 13px; +} +.closeButton { + position: relative; + bottom: 5rem; + right: 10px; + padding: 4px; + background-color: red; /* Red close button color */ + color: #fff; + border: none; + cursor: pointer; +} +.closeButtonP { + position: absolute; + top: 0px; + right: 0px; + background: transparent; + transform: scale(1.2); + cursor: pointer; + border: none; + color: #707070; + font-weight: 600; + font-size: 16px; + cursor: pointer; +} +.cards:hover::after { + opacity: 1; + mix-blend-mode: normal; } .cards > p { font-size: 14px; @@ -25,6 +175,9 @@ } .infodiv { margin-bottom: 7px; + width: 15rem; + text-align: justify; + word-wrap: break-word; } .infodiv > p { margin: 0; @@ -40,8 +193,7 @@ transform: scale(0.75); cursor: pointer; } - -.cards { +/* .cards { width: 75%; background: #fcfcfc; margin: 10px 40px; @@ -52,16 +204,14 @@ margin-right: 30px; color: #737373; box-sizing: border-box; -} +} */ .cards:last-child:nth-last-child(odd) { grid-column: auto / span 2; } - .cards:first-child:nth-last-child(even), .cards:first-child:nth-last-child(even) ~ .box { grid-column: auto / span 1; } - .toggleClickBtn { color: #31bb6b; cursor: pointer; @@ -69,7 +219,60 @@ font-size: 12px; background-color: white; } - .toggleClickBtnNone { display: none; } +/* Menu Modal Styles */ +.menuModal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 0.7); /* Dark grey modal background */ + z-index: 9999; +} + +.menuContent { + display: flex; + align-items: center; + justify-content: center; + background-color: #fff; + padding-top: 20px; + max-width: 700px; + max-height: 500px; + overflow: hidden; + position: relative; +} + +.menuOptions { + list-style-type: none; + padding: 0; + margin: 0; +} + +.menuOptions li { + padding: 10px; + border-bottom: 1px solid #ccc; + padding-left: 100px; + padding-right: 100px; + cursor: pointer; +} + +.moreOptionsButton { + position: relative; + bottom: 5rem; + right: 10px; + padding: 2px; + background-color: transparent; + color: #000; + border: none; + cursor: pointer; +} +.list { + color: red; + cursor: pointer; +} diff --git a/src/components/OrgPostCard/OrgPostCard.test.tsx b/src/components/OrgPostCard/OrgPostCard.test.tsx index dc8a7e5fd4..e2a209c00f 100644 --- a/src/components/OrgPostCard/OrgPostCard.test.tsx +++ b/src/components/OrgPostCard/OrgPostCard.test.tsx @@ -1,19 +1,25 @@ import React from 'react'; -import { act, render, screen, fireEvent } from '@testing-library/react'; +import { + act, + render, + screen, + fireEvent, + waitFor, +} from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; -import userEvent from '@testing-library/user-event'; -import { I18nextProvider } from 'react-i18next'; - import OrgPostCard from './OrgPostCard'; +import { I18nextProvider } from 'react-i18next'; +import userEvent from '@testing-library/user-event'; +import 'jest-localstorage-mock'; import { DELETE_POST_MUTATION, UPDATE_POST_MUTATION, + TOGGLE_PINNED_POST, } from 'GraphQl/Mutations/mutations'; import i18nForTest from 'utils/i18nForTest'; import { StaticMockLink } from 'utils/StaticMockLink'; - -import mockedStyles from './OrgPostCard.module.css'; - +import convertToBase64 from 'utils/convertToBase64'; +import { BrowserRouter } from 'react-router-dom'; const MOCKS = [ { request: { @@ -23,7 +29,7 @@ const MOCKS = [ result: { data: { removePost: { - _id: '1', + _id: '123', }, }, }, @@ -45,7 +51,29 @@ const MOCKS = [ }, }, }, + { + request: { + query: TOGGLE_PINNED_POST, + variables: { + id: '32', + }, + }, + result: { + data: { + togglePostPin: { + _id: '32', + }, + }, + }, + }, ]; +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); const link = new StaticMockLink(MOCKS, true); async function wait(ms = 100): Promise { await act(() => { @@ -54,7 +82,6 @@ async function wait(ms = 100): Promise { }); }); } - describe('Testing Organization Post Card', () => { const props = { key: '123', @@ -62,37 +89,73 @@ describe('Testing Organization Post Card', () => { postTitle: 'Event Info', postInfo: 'Time change', postAuthor: 'John Doe', - postPhoto: 'photoLink', - postVideo: 'videoLink', + postPhoto: 'test.png', + postVideo: 'test.mp4', + pinned: false, }; - + jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, + })); + jest.mock('react', () => ({ + ...jest.requireActual('react'), + useRef: jest.fn(), + })); global.alert = jest.fn(); - test('should render props and text elements test for the page component', async () => { - global.confirm = (): boolean => true; + test('renders with default props', () => { + const { getByAltText, getByTestId } = render( + + + + + + ); + expect(getByTestId('card-text')).toBeInTheDocument(); + expect(getByTestId('card-title')).toBeInTheDocument(); + expect(getByAltText('image')).toBeInTheDocument(); + }); - render( + test('toggles "Read more" button', () => { + const { getByTestId } = render( ); + userEvent.click(screen.getByAltText('image')); + const toggleButton = getByTestId('toggleBtn'); + fireEvent.click(toggleButton); + expect(toggleButton).toHaveTextContent('hide'); + fireEvent.click(toggleButton); + expect(toggleButton).toHaveTextContent('Read more'); + }); + test('opens and closes edit modal', async () => { + localStorage.setItem('id', '123'); + render( + + + + + + ); await wait(); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + userEvent.click(screen.getByTestId('editPostModalBtn')); - expect(screen.getByText(/Author:/i)).toBeInTheDocument(); - expect(screen.getByText(/Video URL:/i)).toBeInTheDocument(); - expect(screen.getByText(props.postTitle)).toBeInTheDocument(); - expect(screen.getByText(props.postInfo)).toBeInTheDocument(); - expect(screen.getByText(props.postAuthor)).toBeInTheDocument(); - expect(screen.getByAltText(/image not found/i)).toBeInTheDocument(); - expect(screen.getByText(props.postVideo)).toBeInTheDocument(); + const createOrgBtn = screen.getByTestId('modalOrganizationHeader'); + expect(createOrgBtn).toBeInTheDocument(); + userEvent.click(createOrgBtn); + userEvent.click(screen.getByTestId('closeOrganizationModal')); }); - test('Should render text elements when props value is not passed', async () => { global.confirm = (): boolean => false; - render( @@ -100,19 +163,11 @@ describe('Testing Organization Post Card', () => { ); - await wait(); - - expect(screen.getByText(/Author:/i)).toBeInTheDocument(); - expect(screen.getByText(/Video URL:/i)).toBeInTheDocument(); - expect(screen.getByText(props.postTitle)).toBeInTheDocument(); - expect(screen.getByText(props.postInfo)).toBeInTheDocument(); - expect(screen.getByText(props.postAuthor)).toBeInTheDocument(); - expect(screen.getByAltText(/image not found/i)).toBeInTheDocument(); - expect(screen.getByText(props.postVideo)).toBeInTheDocument(); + userEvent.click(screen.getByAltText('image')); + expect(screen.getByAltText('Post Image')).toBeInTheDocument(); }); - - test('Testing post update functionality', async () => { + test('Testing post updating after post is updated', async () => { render( @@ -123,14 +178,15 @@ describe('Testing Organization Post Card', () => { await wait(); - userEvent.click(screen.getByTestId('editPostModalBtn')); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + userEvent.click(screen.getByTestId('editPostModalBtn')); userEvent.type(screen.getByTestId('updateTitle'), 'updated title'); userEvent.type(screen.getByTestId('updateText'), 'This is a updated text'); userEvent.click(screen.getByTestId('updatePostBtn')); }); - - test('Testing delete post funcationality', async () => { + test('Testing pin post functionality', async () => { render( @@ -141,11 +197,31 @@ describe('Testing Organization Post Card', () => { await wait(); - userEvent.click(screen.getByTestId('deletePostModalBtn')); - userEvent.click(screen.getByTestId(/deletePostBtn/i)); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('pinpostBtn')); }); + test('Testing post delete functionality', async () => { + render( + + + + + + + + ); - test('should toggle post visibility when button is clicked', () => { + await wait(); + + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('deletePostModalBtn')); + fireEvent.click(screen.getByTestId('deletePostBtn')); + }); + test('Testing close functionality of primary modal', async () => { render( @@ -154,30 +230,37 @@ describe('Testing Organization Post Card', () => { ); - const toggleButton = screen.getByTestId('toggleBtn'); - - expect(screen.getByText('Read more')).toBeInTheDocument(); - - fireEvent.click(toggleButton); + await wait(); - expect(screen.getByText('hide')).toBeInTheDocument(); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('closeiconbtn')); + }); + test('Testing close functionality of secondary modal', async () => { + render( + + + + + + ); - fireEvent.click(toggleButton); + await wait(); - expect(screen.getByText('Read more')).toBeInTheDocument(); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + userEvent.click(screen.getByTestId('closebtn')); }); - - test('should toggle post content', () => { + test('renders without "Read more" button when postInfo length is less than or equal to 43', () => { const props = { key: '123', id: '12', postTitle: 'Event Info', - postInfo: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + postInfo: 'Lorem ipsum dolor sit amet', postAuthor: 'John Doe', postPhoto: 'photoLink', postVideo: 'videoLink', + pinned: false, }; - render( @@ -185,34 +268,100 @@ describe('Testing Organization Post Card', () => { ); + }); + test('updates state variables correctly when handleEditModal is called', () => { + const link2 = new StaticMockLink(MOCKS, true); + render( + + + + ); + userEvent.click(screen.getByAltText('image')); + + userEvent.click(screen.getByTestId('moreiconbtn')); + + expect(screen.queryByTestId('editPostModalBtn')).toBeInTheDocument(); + + userEvent.click(screen.getByTestId('editPostModalBtn')); + + // expect(screen.queryByTestId('editPostModalBtn')).toBeInTheDocument(); + // expect(screen.queryByTestId('deletePostModalBtn')).not.toBeInTheDocument(); + // expect(screen.queryByTestId('closeiconbtn')).not.toBeInTheDocument(); - const toggleBtn = screen.getByRole('toggleBtn'); + // expect(screen.getByTestId('editPostModal')).toHaveClass('show'); + // expect(screen.getByTestId('deletePostModal')).not.toHaveClass('show'); - expect( - screen.getByText('Lorem ipsum dolor sit amet, consectetur ...') - ).toBeInTheDocument(); - expect(toggleBtn).toHaveTextContent('Read more'); - expect(toggleBtn).toHaveClass(mockedStyles.toggleClickBtn); + // expect(screen.getByTestId('modalVisible')).toBe('false'); + // expect(screen.getByTestId('menuVisible')).toBe('false'); + // expect(screen.getByTestId('showEditModal')).toBe('true'); + // expect(screen.getByTestId('showDeleteModal')).toBe('false'); + }); + test('clears postvideo state and resets file input value', async () => { + const { getByTestId } = render( + + + + + + ); - fireEvent.click(toggleBtn); + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); - expect(screen.getByTestId('toggleContent').innerHTML).toEqual( - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' + userEvent.click(screen.getByTestId('editPostModalBtn')); + userEvent.click(screen.getByTestId('closePreview')); + + fireEvent.change(getByTestId('postVideoUrl'), { + target: { value: '' }, + }); + userEvent.click(screen.getByPlaceholderText(/video/i)); + const input = getByTestId('postVideoUrl'); + const file = new File(['test-video'], 'test.mp4', { type: 'video/mp4' }); + Object.defineProperty(input, 'files', { + value: [file], + }); + fireEvent.change(input); + await waitFor(() => { + convertToBase64(file); + }); + }); + test('clears postimage state and resets file input value', async () => { + const { getByTestId } = render( + + + + + ); - expect(toggleBtn).toHaveTextContent('hide'); - expect(toggleBtn).toHaveClass(mockedStyles.toggleClickBtn); + + userEvent.click(screen.getByAltText('image')); + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('editPostModalBtn')); + userEvent.click(screen.getByTestId('closePreview')); + + fireEvent.change(getByTestId('postImageUrl'), { + target: { value: '' }, + }); + userEvent.click(screen.getByPlaceholderText(/image/i)); + const input = getByTestId('postImageUrl'); + const file = new File(['test-image'], 'test.jpg', { type: 'image/jpeg' }); + Object.defineProperty(input, 'files', { + value: [file], + }); + fireEvent.change(input); + + // Simulate the asynchronous base64 conversion function + await waitFor(() => { + convertToBase64(file); // Replace with the expected base64-encoded image + }); + document.getElementById = jest.fn(() => input); + const clearImageButton = getByTestId('closeimage'); + fireEvent.click(clearImageButton); }); + test('Testing create organization modal', async () => { + localStorage.setItem('id', '123'); - test('renders without "Read more" button when postInfo length is less than or equal to 43', () => { - const props = { - key: '123', - id: '12', - postTitle: 'Event Info', - postInfo: 'Lorem ipsum dolor sit amet', - postAuthor: 'John Doe', - postPhoto: 'photoLink', - postVideo: 'videoLink', - }; render( @@ -220,5 +369,35 @@ describe('Testing Organization Post Card', () => { ); + + await wait(); + userEvent.click(screen.getByAltText('image')); + + userEvent.click(screen.getByTestId('moreiconbtn')); + + userEvent.click(screen.getByTestId('editPostModalBtn')); + const createOrgBtn = screen.getByTestId('modalOrganizationHeader'); + expect(createOrgBtn).toBeInTheDocument(); + userEvent.click(createOrgBtn); + userEvent.click(screen.getByTestId('closeOrganizationModal')); + }); + test('should toggle post pin when pin button is clicked', async () => { + const { getByTestId } = render( + + + + + + ); + userEvent.click(screen.getByAltText('image')); + + userEvent.click(screen.getByTestId('moreiconbtn')); + const pinButton = getByTestId('pinpostBtn'); + fireEvent.click(pinButton); + await waitFor(() => { + expect(MOCKS[2].request.variables).toEqual({ + id: '32', + }); + }); }); }); diff --git a/src/components/OrgPostCard/OrgPostCard.tsx b/src/components/OrgPostCard/OrgPostCard.tsx index 3eb2d675f2..5845ef90ba 100644 --- a/src/components/OrgPostCard/OrgPostCard.tsx +++ b/src/components/OrgPostCard/OrgPostCard.tsx @@ -1,19 +1,24 @@ import type { ChangeEvent } from 'react'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { useMutation } from '@apollo/client'; import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; +import Card from 'react-bootstrap/Card'; import { toast } from 'react-toastify'; - +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import CloseIcon from '@mui/icons-material/Close'; +import PushPinIcon from '@mui/icons-material/PushPin'; +import AboutImg from 'assets/images/defaultImg.png'; import { DELETE_POST_MUTATION, UPDATE_POST_MUTATION, + TOGGLE_PINNED_POST, } from 'GraphQl/Mutations/mutations'; -import defaultImg from 'assets/images/blank.png'; import { useTranslation } from 'react-i18next'; import { errorHandler } from 'utils/errorHandler'; import styles from './OrgPostCard.module.css'; import { Form } from 'react-bootstrap'; +import convertToBase64 from 'utils/convertToBase64'; interface InterfaceOrgPostCardProps { key: string; @@ -23,22 +28,103 @@ interface InterfaceOrgPostCardProps { postAuthor: string; postPhoto: string; postVideo: string; + pinned: boolean; } -function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function OrgPostCard( + props: InterfaceOrgPostCardProps +): JSX.Element { const [postformState, setPostFormState] = useState({ posttitle: '', postinfo: '', + postphoto: '', + postvideo: '', + pinned: false, }); const [togglePost, setPostToggle] = useState('Read more'); const [showEditModal, setShowEditModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [menuVisible, setMenuVisible] = useState(false); + const [playing, setPlaying] = useState(false); + const videoRef = useRef(null); + const [toggle] = useMutation(TOGGLE_PINNED_POST); + const togglePostPin = async (id: string, pinned: boolean): Promise => { + try { + const { data } = await toggle({ + variables: { + id, + }, + }); + if (data) { + setModalVisible(false); + setMenuVisible(false); + toast.success(`${pinned ? 'Post unpinned' : 'Post pinned'}`); + setTimeout(() => { + window.location.reload(); + }, 2000); + } + } catch (error: any) { + console.log(error); + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + const toggleShowEditModal = (): void => { + setPostFormState({ + posttitle: props.postTitle, + postinfo: props.postInfo, + postphoto: props.postPhoto, + postvideo: props.postVideo, + pinned: props.pinned, + }); + setShowEditModal((prev) => !prev); + }; + const toggleShowDeleteModal = (): void => setShowDeleteModal((prev) => !prev); + + const handleVideoPlay = (): void => { + setPlaying(true); + videoRef.current?.play(); + }; + + const handleVideoPause = (): void => { + setPlaying(false); + videoRef.current?.pause(); + }; + const handleCardClick = (): void => { + setModalVisible(true); + }; - const toggleShowEditModal = (): void => setShowEditModal(!showEditModal); - const toggleShowDeleteModal = (): void => - setShowDeleteModal(!showDeleteModal); + const handleMoreOptionsClick = (): void => { + setMenuVisible(true); + }; + const clearImageInput = (): void => { + setPostFormState({ + ...postformState, + postphoto: '', + }); + const fileInput = document.getElementById( + 'postImageUrl' + ) as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + }; + const clearVideoInput = (): void => { + setPostFormState({ + ...postformState, + postvideo: '', + }); + const fileInput = document.getElementById( + 'postVideoUrl' + ) as HTMLInputElement; + if (fileInput) { + fileInput.value = ''; + } + }; function handletoggleClick(): void { if (togglePost === 'Read more') { setPostToggle('hide'); @@ -47,10 +133,30 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { } } + function handleEditModal(): void { + setModalVisible(false); + setMenuVisible(false); + setShowEditModal(true); + setPostFormState({ + ...postformState, + postphoto: props.postPhoto, + postvideo: props.postVideo, + }); + } + + function handleDeleteModal(): void { + setModalVisible(false); + setMenuVisible(false); + setShowDeleteModal(true); + } + useEffect(() => { setPostFormState({ posttitle: props.postTitle, postinfo: props.postInfo, + postphoto: props.postPhoto, + postvideo: props.postVideo, + pinned: props.pinned, }); }, []); @@ -58,36 +164,35 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { keyPrefix: 'orgPostCard', }); - const [create] = useMutation(DELETE_POST_MUTATION); - const [updatePost] = useMutation(UPDATE_POST_MUTATION); + const [deletePostMutation] = useMutation(DELETE_POST_MUTATION); + const [updatePostMutation] = useMutation(UPDATE_POST_MUTATION); const deletePost = async (): Promise => { try { - const { data } = await create({ + const { data } = await deletePostMutation({ variables: { id: props.id, }, }); - /* istanbul ignore next */ if (data) { toast.success(t('postDeleted')); setTimeout(() => { window.location.reload(); - }, 2000); + }); } } catch (error: any) { - /* istanbul ignore next */ errorHandler(t, error); } }; - const handleInputEvent = ( e: ChangeEvent ): void => { const { name, value } = e.target; - - setPostFormState({ ...postformState, [name]: value }); + setPostFormState((prevPostFormState) => ({ + ...prevPostFormState, + [name]: value, + })); }; const updatePostHandler = async ( @@ -96,15 +201,27 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { e.preventDefault(); try { - const { data } = await updatePost({ + let imageUrl = null; + let videoUrl = null; + + if (e.target?.postphoto && e.target?.postphoto.files.length > 0) { + imageUrl = postformState.postphoto; + } + + if (e.target?.postvideo && e.target?.postvideo.files.length > 0) { + videoUrl = postformState.postvideo; + } + + const { data } = await updatePostMutation({ variables: { id: props.id, title: postformState.posttitle, text: postformState.postinfo, + imageUrl, + videoUrl, }, }); - /* istanbul ignore next */ if (data) { toast.success(t('postUpdated')); setTimeout(() => { @@ -112,90 +229,221 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { }, 2000); } } catch (error: any) { - /* istanbul ignore next */ toast.error(error.message); } }; return ( <> -
-
-
-

{props.postTitle}

-
+
+
+ {props.postVideo && ( + + + + {props.pinned && ( + + )} + + {props.postTitle} + + + {props.postInfo} + + + {props.postAuthor} + + + + )} {props.postPhoto ? ( -

- - {' '} - + + + {props.pinned && ( + + )} + + {props.postTitle} + + {props.postInfo} + {props.postAuthor} + + + ) : !props.postVideo ? ( + + + - -

+ + {props.pinned && ( + + )} + + {props.postTitle} + + + {props.postInfo && props.postInfo.length > 20 + ? props.postInfo.substring(0, 20) + '...' + : props.postInfo} + {' '} + + {props.postAuthor} + + + + ) : ( - image not found + '' )} -

- {t('author')}: {props.postAuthor} -

-
- {togglePost === 'Read more' ? ( -

- {props.postInfo.length > 43 - ? props.postInfo.substring(0, 40) + '...' - : props.postInfo} -

- ) : ( -

{props.postInfo}

- )} - +
+ {modalVisible && ( +
+
+ {props.postPhoto && ( +
+ Post Image +
+ )} + {props.postVideo && ( +
+ +
+ )} + {!props.postPhoto && !props.postVideo && ( +
+ {' '} + Post Image +
+ )} + +
+

+ {t('author')}: {props.postAuthor} +

+
+ {togglePost === 'Read more' ? ( +

+ {props.postInfo.length > 43 + ? props.postInfo.substring(0, 40) + '...' + : props.postInfo} +

+ ) : ( +

{props.postInfo}

+ )} + +
+
+ + +
-

- {t('videoURL')}: - - {' '} - {props.postVideo} - -

+ )} -
- - + {menuVisible && ( +
+
+
    +
  • + {t('edit')} +
  • +
  • + {t('deletePost')} +
  • +
  • => + togglePostPin(props.id, props.pinned) + } + > + {!props.pinned ? 'Pin post' : 'Unpin post'} +
  • +
  • setMenuVisible(false)} + data-testid="closebtn" + > + {t('close')} +
  • +
+
-
+ )}
{/* Delete Modal */} @@ -223,89 +471,159 @@ function orgPostCard(props: InterfaceOrgPostCardProps): JSX.Element { {/* Edit Modal */} - - -
{t('editPost')}
- + + + {t('editPost')} -
+ -
- - -
-
- - -
-
- - -
-
- - -
+ {t('postTitle')} + + {t('information')} + + {props.postPhoto && ( + <> + {t('image')} + + ): Promise => { + setPostFormState((prevPostFormState) => ({ + ...prevPostFormState, + postphoto: '', + })); + + const file = e.target.files?.[0]; + if (file) { + setPostFormState({ + ...postformState, + postphoto: await convertToBase64(file), + }); + } + }} + /> + {props.postPhoto && ( + <> + {postformState.postphoto && ( +
+ Post Image Preview + +
+ )} + + )} + + )} + {props.postVideo && ( + <> + {t('video')} + + ): Promise => { + setPostFormState((prevPostFormState) => ({ + ...prevPostFormState, + postvideo: '', + })); + const target = e.target as HTMLInputElement; + const file = target.files && target.files[0]; + if (file) { + const videoBase64 = await convertToBase64(file); + setPostFormState({ + ...postformState, + postvideo: videoBase64, + }); + } + }} + /> + {postformState.postvideo && ( +
+ + +
+ )} + + )}
- + -
+
); } -export default orgPostCard; diff --git a/src/components/TableLoader/TableLoader.test.tsx b/src/components/TableLoader/TableLoader.test.tsx index d3e6d88d4d..b6526a65c7 100644 --- a/src/components/TableLoader/TableLoader.test.tsx +++ b/src/components/TableLoader/TableLoader.test.tsx @@ -2,22 +2,27 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; +import type { InterfaceTableLoader } from './TableLoader'; import TableLoader from './TableLoader'; -const props = { - noOfRows: 10, - headerTitles: ['header1', 'header2', 'header3'], -}; +beforeAll(() => { + console.error = jest.fn(); +}); describe('Testing Loader component', () => { - test('Component should be rendered properly', () => { + test('Component should be rendered properly only headerTitles is provided', () => { + const props: InterfaceTableLoader = { + noOfRows: 10, + headerTitles: ['header1', 'header2', 'header3'], + }; render( ); // Check if header titles are rendered properly - props.headerTitles.forEach((title) => { + const data = props.headerTitles as string[]; + data.forEach((title) => { expect(screen.getByText(title)).toBeInTheDocument(); }); @@ -26,11 +31,48 @@ describe('Testing Loader component', () => { expect( screen.getByTestId(`row-${rowIndex}-tableLoading`) ).toBeInTheDocument(); - for (let colIndex = 0; colIndex < props.headerTitles.length; colIndex++) { + for (let colIndex = 0; colIndex < data.length; colIndex++) { + expect( + screen.getByTestId(`row-${rowIndex}-col-${colIndex}-tableLoading`) + ).toBeInTheDocument(); + } + } + }); + test('Component should be rendered properly only noCols is provided', () => { + const props: InterfaceTableLoader = { + noOfRows: 10, + noOfCols: 3, + }; + render( + + + + ); + // Check if header titles are rendered properly + const data = [...Array(props.noOfCols)]; + + // Check if elements are rendered properly + for (let rowIndex = 0; rowIndex < props.noOfRows; rowIndex++) { + expect( + screen.getByTestId(`row-${rowIndex}-tableLoading`) + ).toBeInTheDocument(); + for (let colIndex = 0; colIndex < data.length; colIndex++) { expect( screen.getByTestId(`row-${rowIndex}-col-${colIndex}-tableLoading`) ).toBeInTheDocument(); } } }); + test('Component should be throw error when noOfCols and headerTitles are undefined', () => { + const props = { + noOfRows: 10, + }; + expect(() => { + render( + + + + ); + }).toThrowError(); + }); }); diff --git a/src/components/TableLoader/TableLoader.tsx b/src/components/TableLoader/TableLoader.tsx index 49d317b292..2da4b1d172 100644 --- a/src/components/TableLoader/TableLoader.tsx +++ b/src/components/TableLoader/TableLoader.tsx @@ -1,25 +1,40 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import styles from './TableLoader.module.css'; import { Table } from 'react-bootstrap'; -interface InterfaceTableLoader { +export interface InterfaceTableLoader { noOfRows: number; - headerTitles: string[]; + headerTitles?: string[]; + noOfCols?: number; } const tableLoader = (props: InterfaceTableLoader): JSX.Element => { - const { noOfRows, headerTitles } = props; + const { noOfRows, headerTitles, noOfCols } = props; + + useEffect(() => { + if (headerTitles == undefined && noOfCols == undefined) { + throw new Error( + 'TableLoader error Either headerTitles or noOfCols is required !' + ); + } + }, []); return ( <> - +
- {headerTitles.map((title, index) => { - return ; - })} + {headerTitles + ? headerTitles.map((title, index) => { + return ; + }) + : noOfCols && + [...Array(noOfCols)].map((_, index) => { + return + {[...Array(noOfRows)].map((_, rowIndex) => { return ( @@ -28,16 +43,18 @@ const tableLoader = (props: InterfaceTableLoader): JSX.Element => { className="mb-4" data-testid={`row-${rowIndex}-tableLoading`} > - {[...Array(headerTitles?.length)].map((_, colIndex) => { - return ( - - ); - })} + {[...Array(headerTitles ? headerTitles?.length : noOfCols)].map( + (_, colIndex) => { + return ( + + ); + } + )} ); })} diff --git a/src/components/UsersTableItem/UserTableItem.test.tsx b/src/components/UsersTableItem/UserTableItem.test.tsx new file mode 100644 index 0000000000..0e67a02022 --- /dev/null +++ b/src/components/UsersTableItem/UserTableItem.test.tsx @@ -0,0 +1,1054 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/react-testing'; +import { act, fireEvent, render, screen } from '@testing-library/react'; +import { I18nextProvider } from 'react-i18next'; +import { toast } from 'react-toastify'; +import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import type { InterfaceQueryUserListItem } from 'utils/interfaces'; +import { MOCKS } from './UserTableItemMocks'; +import UsersTableItem from './UsersTableItem'; + +const link = new StaticMockLink(MOCKS, true); + +async function wait(ms = 100): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); +} +const resetAndRefetchMock = jest.fn(); + +jest.mock('react-toastify', () => ({ + toast: { + success: jest.fn(), + error: jest.fn(), + warning: jest.fn(), + }, +})); + +Object.defineProperty(window, 'location', { + value: { + replace: jest.fn(), + }, + writable: true, +}); + +const mockHistoryPush = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => ({ + push: mockHistoryPush, + }), +})); + +beforeEach(() => { + localStorage.setItem('UserType', 'SUPERADMIN'); + localStorage.setItem('id', '123'); +}); + +afterEach(() => { + localStorage.clear(); + jest.clearAllMocks(); +}); + +describe('Testing User Table Item', () => { + console.error = jest.fn((message) => { + if (message.includes('validateDOMNesting')) { + return; + } + // Log other console errors + console.warn(message); + }); + test('Should render props and text elements test for the page component', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + ], + createdAt: '2023-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'XYZ', + image: null, + location: 'Jamaica', + createdAt: '2023-01-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'MNO', + image: null, + location: 'Jamaica', + createdAt: '2023-01-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-06-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-07-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + expect(screen.getByText(/1/i)).toBeInTheDocument(); + expect(screen.getByText(/John Doe/i)).toBeInTheDocument(); + expect(screen.getByText(/john@example.com/i)).toBeInTheDocument(); + expect(screen.getByTestId(`changeRole${123}`)).toBeInTheDocument(); + expect(screen.getByTestId(`changeRole${123}`)).toHaveValue( + `SUPERADMIN?${123}` + ); + expect(screen.getByTestId(`showJoinedOrgsBtn${123}`)).toBeInTheDocument(); + expect( + screen.getByTestId(`showBlockedByOrgsBtn${123}`) + ).toBeInTheDocument(); + }); + + test('Should render elements correctly when JoinedOrgs and BlockedByOrgs are empty', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + ], + createdAt: '2023-09-29T15:39:36.355Z', + organizationsBlockedBy: [], + joinedOrganizations: [], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + render( + + + + + + ); + + await wait(); + const showJoinedOrgsBtn = screen.getByTestId(`showJoinedOrgsBtn${123}`); // 123 is userId + const showBlockedByOrgsBtn = screen.getByTestId( + `showBlockedByOrgsBtn${123}` + ); // 123 is userId + + // Open JoinedOrgs Modal -> Expect modal to contain text and no search box -> Close Modal + fireEvent.click(showJoinedOrgsBtn); + expect( + screen.queryByTestId(`searchByNameJoinedOrgs`) + ).not.toBeInTheDocument(); + expect( + screen.getByText(/John Doe has not joined any organization/i) + ).toBeInTheDocument(); + fireEvent.click(screen.getByTestId(`closeJoinedOrgsBtn${123}`)); + + // Open BlockedByOrgs Modal -> Expect modal to contain text and no search box -> Close Modal + fireEvent.click(showBlockedByOrgsBtn); + expect( + screen.queryByTestId(`searchByNameOrgsBlockedBy`) + ).not.toBeInTheDocument(); + expect( + screen.getByText(/John Doe is not blocked by any organization/i) + ).toBeInTheDocument(); + fireEvent.click(screen.getByTestId(`closeBlockedByOrgsBtn${123}`)); + }); + + test('Should render props and text elements test for the Joined Organizations Modal properly', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Joined%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showJoinedOrgsBtn = screen.getByTestId(`showJoinedOrgsBtn${123}`); + expect(showJoinedOrgsBtn).toBeInTheDocument(); + fireEvent.click(showJoinedOrgsBtn); + expect(screen.getByTestId('modal-joined-org-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-joined-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + fireEvent.click(showJoinedOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId(`closeJoinedOrgsBtn${123}`)); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + + fireEvent.click(showJoinedOrgsBtn); + + // Expect the following to exist in modal + const inputBox = screen.getByTestId(`searchByNameJoinedOrgs`); + expect(inputBox).toBeInTheDocument(); + expect(screen.getByText(/Joined Organization 1/i)).toBeInTheDocument(); + expect(screen.getByText(/Joined Organization 2/i)).toBeInTheDocument(); + expect(screen.getAllByText(/Jamaica/i)).toHaveLength(2); + expect(screen.getByText(/29-08-2023/i)).toBeInTheDocument(); + expect(screen.getByText(/19-09-2023/i)).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtnabc')).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtndef')).toBeInTheDocument(); + expect(screen.getByTestId(`changeRoleInOrgabc`)).toHaveValue('ADMIN?abc'); + expect(screen.getByTestId(`changeRoleInOrgdef`)).toHaveValue('USER?def'); + + // Search for Joined Organization 1 + fireEvent.change(inputBox, { target: { value: 'Joined Organization 1' } }); + expect(screen.getByText(/Joined Organization 1/i)).toBeInTheDocument(); + expect( + screen.queryByText(/Joined Organization 2/i) + ).not.toBeInTheDocument(); + + // Search for an Organization which does not exist + fireEvent.change(inputBox, { target: { value: 'Joined Organization 3' } }); + expect( + screen.getByText(`No results found for "Joined Organization 3"`) + ).toBeInTheDocument(); + + // Now clear the search box + fireEvent.change(inputBox, { target: { value: '' } }); + + // Click on Creator Link + fireEvent.click(screen.getByTestId(`creatorabc`)); + expect(toast.success).toBeCalledWith('Profile Page Coming Soon !'); + + // Click on Organization Link + fireEvent.click(screen.getByText(/Joined Organization 1/i)); + expect(window.location.replace).toBeCalledWith('/orgdash/id=abc'); + expect(mockHistoryPush).toBeCalledWith('/orgdash/id=abc'); + fireEvent.click(screen.getByTestId(`closeJoinedOrgsBtn${123}`)); + }); + + test('Should render props and text elements test for the Blocked By Organizations Modal properly', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showBlockedByOrgsBtn = screen.getByTestId( + `showBlockedByOrgsBtn${123}` + ); + expect(showBlockedByOrgsBtn).toBeInTheDocument(); + fireEvent.click(showBlockedByOrgsBtn); + expect(screen.getByTestId('modal-blocked-org-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-blocked-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + fireEvent.click(showBlockedByOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId(`closeBlockedByOrgsBtn${123}`)); + expect( + screen.queryByRole('dialog')?.className.includes('show') + ).toBeFalsy(); + + fireEvent.click(showBlockedByOrgsBtn); + + // Expect the following to exist in modal + + const inputBox = screen.getByTestId(`searchByNameOrgsBlockedBy`); + expect(inputBox).toBeInTheDocument(); + expect(screen.getByText(/Blocked Organization 1/i)).toBeInTheDocument(); + expect(screen.getByText(/Blocked Organization 2/i)).toBeInTheDocument(); + expect(screen.getAllByText(/Jamaica/i)).toHaveLength(2); + expect(screen.getByText(/29-08-2023/i)).toBeInTheDocument(); + expect(screen.getByText(/29-09-2023/i)).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtnxyz')).toBeInTheDocument(); + expect(screen.getByTestId('removeUserFromOrgBtnmno')).toBeInTheDocument(); + expect(screen.getByTestId(`changeRoleInOrgxyz`)).toHaveValue('ADMIN?xyz'); + expect(screen.getByTestId(`changeRoleInOrgmno`)).toHaveValue('USER?mno'); + // Click on Creator Link + fireEvent.click(screen.getByTestId(`creatorxyz`)); + expect(toast.success).toBeCalledWith('Profile Page Coming Soon !'); + + // Search for Blocked Organization 1 + fireEvent.change(inputBox, { target: { value: 'Blocked Organization 1' } }); + expect(screen.getByText(/Blocked Organization 1/i)).toBeInTheDocument(); + expect( + screen.queryByText(/Blocked Organization 2/i) + ).not.toBeInTheDocument(); + + // Search for an Organization which does not exist + fireEvent.change(inputBox, { target: { value: 'Blocked Organization 3' } }); + expect( + screen.getByText(`No results found for "Blocked Organization 3"`) + ).toBeInTheDocument(); + + // Now clear the search box + fireEvent.change(inputBox, { target: { value: '' } }); + + // Click on Organization Link + fireEvent.click(screen.getByText(/Blocked Organization 1/i)); + expect(window.location.replace).toBeCalledWith('/orgdash/id=xyz'); + expect(mockHistoryPush).toBeCalledWith('/orgdash/id=xyz'); + fireEvent.click(screen.getByTestId(`closeBlockedByOrgsBtn${123}`)); + }); + + test('Remove user from Organization should function properly in Organizations Joined Modal', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showJoinedOrgsBtn = screen.getByTestId(`showJoinedOrgsBtn${123}`); + expect(showJoinedOrgsBtn).toBeInTheDocument(); + fireEvent.click(showJoinedOrgsBtn); + expect(screen.getByTestId('modal-joined-org-123')).toBeInTheDocument(); + fireEvent.click(showJoinedOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'abc'}`)); + expect(screen.getByTestId('modal-remove-user-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-joined-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + fireEvent.click(showJoinedOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId('closeRemoveUserModal123')); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + + fireEvent.click(showJoinedOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'abc'}`)); + const confirmRemoveBtn = screen.getByTestId(`confirmRemoveUser123`); + expect(confirmRemoveBtn).toBeInTheDocument(); + + fireEvent.click(confirmRemoveBtn); + }); + + test('Remove user from Organization should function properly in Organizations Blocked by Modal', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'SUPERADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + const showBlockedByOrgsBtn = screen.getByTestId( + `showBlockedByOrgsBtn${123}` + ); + expect(showBlockedByOrgsBtn).toBeInTheDocument(); + fireEvent.click(showBlockedByOrgsBtn); + expect(screen.getByTestId('modal-blocked-org-123')).toBeInTheDocument(); + fireEvent.click(showBlockedByOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'xyz'}`)); + expect(screen.getByTestId('modal-remove-user-123')).toBeInTheDocument(); + + // Close using escape key and reopen + fireEvent.keyDown(screen.getByTestId('modal-blocked-org-123'), { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + fireEvent.click(showBlockedByOrgsBtn); + // Close using close button and reopen + fireEvent.click(screen.getByTestId('closeRemoveUserModal123')); + expect( + screen + .queryAllByRole('dialog') + .some((el) => el.className.includes('show')) + ).toBeTruthy(); + + fireEvent.click(showBlockedByOrgsBtn); + fireEvent.click(screen.getByTestId(`removeUserFromOrgBtn${'xyz'}`)); + const confirmRemoveBtn = screen.getByTestId(`confirmRemoveUser123`); + expect(confirmRemoveBtn).toBeInTheDocument(); + + fireEvent.click(confirmRemoveBtn); + }); + + test('Should be able to change userType of a user if not self', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'USER', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '456', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + fireEvent.select(screen.getByTestId(`changeRole123`), { + target: { value: 'ADMIN?123' }, + }); + }); + + test('Should be not able to change userType of self', async () => { + const props: { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; + } = { + user: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + userType: 'ADMIN', + adminApproved: true, + adminFor: [ + { + _id: 'abc', + }, + { + _id: 'xyz', + }, + ], + createdAt: '2022-09-29T15:39:36.355Z', + organizationsBlockedBy: [ + { + _id: 'xyz', + name: 'Blocked Organization 1', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=Blocked%20Organization%201', + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: + 'https://api.dicebear.com/5.x/initials/svg?seed=John%20Doe', + email: 'john@example.com', + }, + }, + { + _id: 'mno', + name: 'Blocked Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + joinedOrganizations: [ + { + _id: 'abc', + name: 'Joined Organization 1', + image: null, + location: 'Jamaica', + createdAt: '2023-08-29T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + { + _id: 'def', + name: 'Joined Organization 2', + image: null, + location: 'Jamaica', + createdAt: '2023-09-19T15:39:36.355Z', + creator: { + _id: '123', + firstName: 'John', + lastName: 'Doe', + image: null, + email: 'john@example.com', + }, + }, + ], + }, + index: 0, + loggedInUserId: '123', + resetAndRefetch: resetAndRefetchMock, + }; + + render( + + + + + + ); + + await wait(); + expect(screen.getByTestId(`changeRole123`)).toBeDisabled(); + expect(screen.getByTestId(`changeRole123`)).toHaveValue('ADMIN?123'); + }); +}); diff --git a/src/components/UsersTableItem/UserTableItemMocks.ts b/src/components/UsersTableItem/UserTableItemMocks.ts new file mode 100644 index 0000000000..6fd90d2be4 --- /dev/null +++ b/src/components/UsersTableItem/UserTableItemMocks.ts @@ -0,0 +1,59 @@ +import { + REMOVE_MEMBER_MUTATION, + UPDATE_USERTYPE_MUTATION, + UPDATE_USER_ROLE_IN_ORG_MUTATION, +} from 'GraphQl/Mutations/mutations'; + +export const MOCKS = [ + { + request: { + query: UPDATE_USERTYPE_MUTATION, + variables: { + id: '123', + userType: 'ADMIN', + }, + }, + result: { + data: { + updateUserType: { + data: { + id: '123', + }, + }, + }, + }, + }, + { + request: { + query: REMOVE_MEMBER_MUTATION, + variables: { + userid: '123', + orgid: 'abc', + }, + }, + result: { + data: { + removeUserFromOrganization: { + _id: '123', + }, + }, + }, + }, + { + request: { + query: UPDATE_USER_ROLE_IN_ORG_MUTATION, + variables: { + userId: '123', + organizationId: 'abc', + role: 'ADMIN', + }, + }, + result: { + data: { + updateUserRoleInOrganization: { + _id: '123', + }, + }, + }, + }, +]; diff --git a/src/components/UsersTableItem/UsersTableItem.module.css b/src/components/UsersTableItem/UsersTableItem.module.css new file mode 100644 index 0000000000..d5fad679a7 --- /dev/null +++ b/src/components/UsersTableItem/UsersTableItem.module.css @@ -0,0 +1,26 @@ +.input { + position: relative; +} + +.notJoined { + height: 300px; + display: flex; + justify-content: center; + align-items: center; +} + +.modalTable img[alt='creator'] { + height: 24px; + width: 24px; + object-fit: contain; + border-radius: 12px; + margin-right: 0.4rem; +} + +.modalTable img[alt='orgImage'] { + height: 28px; + width: 28px; + object-fit: contain; + border-radius: 4px; + margin-right: 0.4rem; +} diff --git a/src/components/UsersTableItem/UsersTableItem.tsx b/src/components/UsersTableItem/UsersTableItem.tsx new file mode 100644 index 0000000000..dbf51e809a --- /dev/null +++ b/src/components/UsersTableItem/UsersTableItem.tsx @@ -0,0 +1,598 @@ +import { useMutation } from '@apollo/client'; +import { Search } from '@mui/icons-material'; +import { + REMOVE_MEMBER_MUTATION, + UPDATE_USERTYPE_MUTATION, + UPDATE_USER_ROLE_IN_ORG_MUTATION, +} from 'GraphQl/Mutations/mutations'; +import dayjs from 'dayjs'; +import React, { useState } from 'react'; +import { Button, Form, Modal, Row, Table } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { useHistory } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { errorHandler } from 'utils/errorHandler'; +import type { InterfaceQueryUserListItem } from 'utils/interfaces'; +import styles from './UsersTableItem.module.css'; + +type Props = { + user: InterfaceQueryUserListItem; + index: number; + loggedInUserId: string; + resetAndRefetch: () => void; +}; + +const UsersTableItem = (props: Props): JSX.Element => { + const { t } = useTranslation('translation', { keyPrefix: 'users' }); + const { user, index, loggedInUserId, resetAndRefetch } = props; + + const [showJoinedOrganizations, setShowJoinedOrganizations] = useState(false); + const [showBlockedOrganizations, setShowBlockedOrganizations] = + useState(false); + const [showRemoveUserModal, setShowRemoveUserModal] = useState(false); + const [removeUserProps, setremoveUserProps] = useState<{ + orgName: string; + orgId: string; + setShowOnCancel: 'JOINED' | 'BLOCKED' | ''; + }>({ + orgName: '', + orgId: '', + setShowOnCancel: '', + }); + const [joinedOrgs, setJoinedOrgs] = useState(user.joinedOrganizations); + const [orgsBlockedBy, setOrgsBlockedBy] = useState( + user.organizationsBlockedBy + ); + const [searchByNameJoinedOrgs, setSearchByNameJoinedOrgs] = useState(''); + const [searchByNameOrgsBlockedBy, setSearchByNameOrgsBlockedBy] = + useState(''); + const [updateUserType] = useMutation(UPDATE_USERTYPE_MUTATION); + const [removeUser] = useMutation(REMOVE_MEMBER_MUTATION); + const [updateUserInOrgType] = useMutation(UPDATE_USER_ROLE_IN_ORG_MUTATION); + const history = useHistory(); + + /* istanbul ignore next */ + const changeRole = async (e: any): Promise => { + const { value } = e.target; + + const inputData = value.split('?'); + + try { + const { data } = await updateUserType({ + variables: { + id: inputData[1], + userType: inputData[0], + }, + }); + if (data) { + toast.success(t('roleUpdated')); + resetAndRefetch(); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + const confirmRemoveUser = async (): Promise => { + try { + const { data } = await removeUser({ + variables: { + userid: user._id, + orgid: removeUserProps.orgId, + }, + }); + + if (data) { + toast.success('Removed User from Organization successfully'); + resetAndRefetch(); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + /* istanbul ignore next */ + const changeRoleInOrg = async (e: any): Promise => { + const { value } = e.target; + + const inputData = value.split('?'); + + try { + const { data } = await updateUserInOrgType({ + variables: { + userId: user._id, + role: inputData[0], + organizationId: inputData[1], + }, + }); + if (data) { + toast.success(t('roleUpdated')); + resetAndRefetch(); + } + } catch (error: any) { + /* istanbul ignore next */ + errorHandler(t, error); + } + }; + + function goToOrg(_id: string): void { + const url = '/orgdash/id=' + _id; + + // Dont change the below two lines + window.location.replace(url); + history.push(url); + } + function handleCreator(): void { + toast.success('Profile Page Coming Soon !'); + } + function handleSearchJoinedOrgs(e: any): void { + const { value } = e.target; + setSearchByNameJoinedOrgs(value); + if (value == '') { + setJoinedOrgs(user.joinedOrganizations); + } else { + const filteredOrgs = user.joinedOrganizations.filter((org) => + org.name.toLowerCase().includes(value.toLowerCase()) + ); + setJoinedOrgs(filteredOrgs); + } + } + function handleSearcgByOrgsBlockedBy(e: any): void { + const { value } = e.target; + setSearchByNameOrgsBlockedBy(value); + if (value == '') { + setOrgsBlockedBy(user.organizationsBlockedBy); + } else { + const filteredOrgs = user.organizationsBlockedBy.filter((org) => + org.name.toLowerCase().includes(value.toLowerCase()) + ); + setOrgsBlockedBy(filteredOrgs); + } + } + + /* istanbul ignore next */ + function onHideRemoveUserModal(): void { + setShowRemoveUserModal(false); + if (removeUserProps.setShowOnCancel == 'JOINED') { + setShowJoinedOrganizations(true); + } else if (removeUserProps.setShowOnCancel == 'BLOCKED') { + setShowBlockedOrganizations(true); + } + } + return ( + <> + {/* Table Item */} + + + + + + + + + + {/* Organizations joined modal */} + setShowJoinedOrganizations(false)} + > + + + {t('orgJoinedBy')} {`${user.firstName}`} {`${user.lastName}`} ( + {user.joinedOrganizations.length}) + + + + {user.joinedOrganizations.length !== 0 && ( +
+ + +
+ )} + + {user.joinedOrganizations.length == 0 ? ( +
+

+ {user.firstName} {user.lastName} {t('hasNotJoinedAnyOrg')} +

+
+ ) : joinedOrgs.length == 0 ? ( + <> +
+

+ {t('noResultsFoundFor')} "{searchByNameJoinedOrgs} + " +

+
+ + ) : ( +
{title}{title}; + })}
-
-
+
+
{index + 1}{`${user.firstName} ${user.lastName}`}{user.email} + + + + + + + + + +
+ + + + + + + + + + + + + {joinedOrgs.map((org) => { + // Check user is admin for this organization or not + let isAdmin = false; + user.adminFor.map((item) => { + if (item._id == org._id) { + isAdmin = true; + } + }); + return ( + + + + + + + + + + ); + })} + +
NameLocationCreated onCreated ByUsers RoleChange RoleAction
+ + {org.location}{dayjs(org.createdAt).format('DD-MM-YYYY')} + + {isAdmin ? 'ADMIN' : 'USER'} + + {isAdmin ? ( + <> + + + + ) : ( + <> + + + + )} + + + +
+ )} + + + + + +
+ {/* Organizations blocked by modal */} + setShowBlockedOrganizations(false)} + data-testid={`modal-blocked-org-${user._id}`} + > + + + {t('orgThatBlocked')} {`${user.firstName}`} {`${user.lastName}`} ( + {user.organizationsBlockedBy.length}) + + + + {user.organizationsBlockedBy.length !== 0 && ( +
+ + +
+ )} + + {user.organizationsBlockedBy.length == 0 ? ( +
+

+ {user.firstName} {user.lastName} {t('isNotBlockedByAnyOrg')} +

+
+ ) : orgsBlockedBy.length == 0 ? ( + <> +
+

+ {t('noResultsFoundFor')} "{searchByNameOrgsBlockedBy} + " +

+
+ + ) : ( + + + + + + + + + + + + + + + {orgsBlockedBy.map((org) => { + // Check user is admin for this organization or not + let isAdmin = false; + user.adminFor.map((item) => { + if (item._id == org._id) { + isAdmin = true; + } + }); + return ( + + + + + + + + + + ); + })} + +
NameLocationCreated onCreated ByUsers RoleChange RoleAction
+ + {org.location}{dayjs(org.createdAt).format('DD-MM-YYYY')} + + {isAdmin ? 'ADMIN' : 'USER'} + + {isAdmin ? ( + <> + + + + ) : ( + <> + + + + )} + + + +
+ )} +
+
+ + + +
+ {/* Remove user from Organization modal */} + onHideRemoveUserModal()} + > + + + Remove User from {removeUserProps.orgName} + + + +

+ Are you sure you want to remove{' '} + + “{user.firstName} {user.lastName}” + {' '} + from organization{' '} + + “ + {removeUserProps.orgName}” + {' '} + ? +

+
+ + + + +
+ + ); +}; + +export default UsersTableItem; diff --git a/src/screens/ForgotPassword/ForgotPassword.module.css b/src/screens/ForgotPassword/ForgotPassword.module.css index e69de29bb2..74e09aecc6 100644 --- a/src/screens/ForgotPassword/ForgotPassword.module.css +++ b/src/screens/ForgotPassword/ForgotPassword.module.css @@ -0,0 +1,71 @@ +.pageWrapper { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100vh; +} + +.cardBody { + padding: 2rem; + background-color: #fff; + border-radius: 0.8rem; + border: 1px solid var(--bs-gray-200); +} + +.keyWrapper { + height: 72px; + width: 72px; + transform: rotate(180deg); + position: relative; + overflow: hidden; + display: block; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + margin: 1rem auto; +} + +.keyWrapper .themeOverlay { + position: absolute; + background-color: var(--bs-primary); + height: 100%; + width: 100%; + opacity: 0.15; +} + +.keyWrapper .keyLogo { + height: 42px; + width: 42px; + -webkit-animation: zoomIn 0.3s ease-in-out; + animation: zoomIn 0.3s ease-in-out; +} + +@-webkit-keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} diff --git a/src/screens/ForgotPassword/ForgotPassword.test.tsx b/src/screens/ForgotPassword/ForgotPassword.test.tsx index 0c61fa90ec..94db5b2839 100644 --- a/src/screens/ForgotPassword/ForgotPassword.test.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.test.tsx @@ -1,21 +1,21 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { act, render, screen } from '@testing-library/react'; -import { Provider } from 'react-redux'; -import { BrowserRouter } from 'react-router-dom'; import userEvent from '@testing-library/user-event'; import 'jest-localstorage-mock'; import 'jest-location-mock'; import { I18nextProvider } from 'react-i18next'; +import { Provider } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; -import ForgotPassword from './ForgotPassword'; -import { store } from 'state/store'; import { FORGOT_PASSWORD_MUTATION, GENERATE_OTP_MUTATION, } from 'GraphQl/Mutations/mutations'; -import i18nForTest from 'utils/i18nForTest'; +import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; +import i18nForTest from 'utils/i18nForTest'; +import ForgotPassword from './ForgotPassword'; const MOCKS = [ { @@ -57,7 +57,9 @@ async function wait(ms = 100): Promise { }); }); } - +beforeEach(() => { + localStorage.setItem('IsLoggedIn', 'FALSE'); +}); afterEach(() => { localStorage.clear(); }); @@ -81,7 +83,9 @@ describe('Testing Forgot Password screen', () => { await wait(); expect(screen.getByText(/Forgot Password/i)).toBeInTheDocument(); - expect(screen.getByText(/Back to Home/i)).toBeInTheDocument(); + expect(screen.getByText(/Registered Email/i)).toBeInTheDocument(); + expect(screen.getByText(/Get Otp/i)).toBeInTheDocument(); + expect(screen.getByText(/Back to Login/i)).toBeInTheDocument(); expect(window.location).toBeAt('/orglist'); }); @@ -128,6 +132,7 @@ describe('Testing Forgot Password screen', () => { ); userEvent.click(screen.getByText('Get OTP')); + await wait(); }); test('Testing forgot password functionality', async () => { @@ -135,6 +140,7 @@ describe('Testing Forgot Password screen', () => { userOtp: '12345', newPassword: 'johnDoe', confirmNewPassword: 'johnDoe', + email: 'johndoe@gmail.com', }; render( @@ -151,18 +157,28 @@ describe('Testing Forgot Password screen', () => { await wait(); + userEvent.type( + screen.getByPlaceholderText(/Registered email/i), + formData.email + ); + + userEvent.click(screen.getByText('Get OTP')); + await wait(); + userEvent.type(screen.getByPlaceholderText('e.g. 12345'), formData.userOtp); userEvent.type(screen.getByTestId('newPassword'), formData.newPassword); userEvent.type( screen.getByTestId('confirmNewPassword'), formData.confirmNewPassword ); - + localStorage.setItem('otpToken', 'lorem ipsum'); userEvent.click(screen.getByText('Change Password')); + await wait(); }); test('Testing forgot password functionality, when new password and confirm password is not same', async () => { const formData = { + email: 'johndoe@gmail.com', userOtp: '12345', newPassword: 'johnDoe', confirmNewPassword: 'doeJohn', @@ -182,6 +198,14 @@ describe('Testing Forgot Password screen', () => { await wait(); + userEvent.type( + screen.getByPlaceholderText(/Registered email/i), + formData.email + ); + + userEvent.click(screen.getByText('Get OTP')); + await wait(); + userEvent.type(screen.getByPlaceholderText('e.g. 12345'), formData.userOtp); userEvent.type(screen.getByTestId('newPassword'), formData.newPassword); userEvent.type( @@ -197,6 +221,7 @@ describe('Testing Forgot Password screen', () => { userOtp: '12345', newPassword: 'johnDoe', confirmNewPassword: 'johnDoe', + email: 'johndoe@gmail.com', }; localStorage.setItem('otpToken', ''); @@ -215,13 +240,20 @@ describe('Testing Forgot Password screen', () => { await wait(); + userEvent.type( + screen.getByPlaceholderText(/Registered email/i), + formData.email + ); + + userEvent.click(screen.getByText('Get OTP')); + await wait(); + userEvent.type(screen.getByPlaceholderText('e.g. 12345'), formData.userOtp); userEvent.type(screen.getByTestId('newPassword'), formData.newPassword); userEvent.type( screen.getByTestId('confirmNewPassword'), formData.confirmNewPassword ); - userEvent.click(screen.getByText('Change Password')); }); }); diff --git a/src/screens/ForgotPassword/ForgotPassword.tsx b/src/screens/ForgotPassword/ForgotPassword.tsx index eb96f70f26..10b4c4db0b 100644 --- a/src/screens/ForgotPassword/ForgotPassword.tsx +++ b/src/screens/ForgotPassword/ForgotPassword.tsx @@ -1,6 +1,6 @@ +import { useMutation } from '@apollo/client'; import type { ChangeEvent } from 'react'; import React, { useEffect, useState } from 'react'; -import { useMutation } from '@apollo/client'; import { Link } from 'react-router-dom'; import { toast } from 'react-toastify'; @@ -8,13 +8,15 @@ import { FORGOT_PASSWORD_MUTATION, GENERATE_OTP_MUTATION, } from 'GraphQl/Mutations/mutations'; +import { ReactComponent as KeyLogo } from 'assets/svgs/key.svg'; -import styles from './ForgotPassword.module.css'; +import ArrowRightAlt from '@mui/icons-material/ArrowRightAlt'; +import Loader from 'components/Loader/Loader'; +import { Form } from 'react-bootstrap'; +import Button from 'react-bootstrap/Button'; import { useTranslation } from 'react-i18next'; import { errorHandler } from 'utils/errorHandler'; -import Button from 'react-bootstrap/Button'; -import { Form } from 'react-bootstrap'; -import Loader from 'components/Loader/Loader'; +import styles from './ForgotPassword.module.css'; const ForgotPassword = (): JSX.Element => { const { t } = useTranslation('translation', { @@ -23,8 +25,10 @@ const ForgotPassword = (): JSX.Element => { document.title = t('title'); - const [componentLoader, setComponentLoader] = useState(true); + const [showEnterEmail, setShowEnterEmail] = useState(true); + const [registeredEmail, setregisteredEmail] = useState(''); + const [forgotPassFormData, setForgotPassFormData] = useState({ userOtp: '', newPassword: '', @@ -35,13 +39,14 @@ const ForgotPassword = (): JSX.Element => { const [forgotPassword, { loading: forgotPasswordLoading }] = useMutation( FORGOT_PASSWORD_MUTATION ); - + const isLoggedIn = localStorage.getItem('IsLoggedIn'); useEffect(() => { - const isLoggedIn = localStorage.getItem('IsLoggedIn'); if (isLoggedIn == 'TRUE') { window.location.replace('/orglist'); } - setComponentLoader(false); + return () => { + localStorage.removeItem('otpToken'); + }; }, []); const getOTP = async (e: ChangeEvent): Promise => { @@ -54,13 +59,12 @@ const ForgotPassword = (): JSX.Element => { }, }); - /* istanbul ignore next */ if (data) { localStorage.setItem('otpToken', data.otp.otpToken); toast.success(t('OTPsent')); + setShowEnterEmail(false); } } catch (error: any) { - /* istanbul ignore next */ if (error.message === 'User not found') { toast.warn(t('emailNotRegistered')); } else if (error.message === 'Failed to fetch') { @@ -100,7 +104,7 @@ const ForgotPassword = (): JSX.Element => { /* istanbul ignore next */ if (data) { toast.success(t('passwordChanges')); - + setShowEnterEmail(true); setForgotPassFormData({ userOtp: '', newPassword: '', @@ -108,150 +112,135 @@ const ForgotPassword = (): JSX.Element => { }); } } catch (error: any) { + setShowEnterEmail(true); /* istanbul ignore next */ errorHandler(t, error); } }; - if (componentLoader || otpLoading || forgotPasswordLoading) { + if (otpLoading || forgotPasswordLoading) { return ; } - return ( -
-
-
-
-

{t('forgotPassword')}

-
- -
-
-
- -
- setregisteredEmail(e.target.value)} - /> -
-
- -
+ <> +
+
+
+
+
+
+
- -
- -
-
-
- -
- - setForgotPassFormData({ - ...forgotPassFormData, - userOtp: e.target.value, - }) - } - /> +

{t('forgotPassword')}

+ {showEnterEmail ? ( +
+ + + {t('registeredEmail')}: + +
+ + setregisteredEmail(e.target.value) + } + /> +
+ +
-
-
- -
- - setForgotPassFormData({ - ...forgotPassFormData, - newPassword: e.target.value, - }) - } - /> + ) : ( +
+
+ {t('enterOtp')}: + + setForgotPassFormData({ + ...forgotPassFormData, + userOtp: e.target.value, + }) + } + /> + + {t('enterNewPassword')}: + + + setForgotPassFormData({ + ...forgotPassFormData, + newPassword: e.target.value, + }) + } + /> + + {t('cofirmNewPassword')}: + + + setForgotPassFormData({ + ...forgotPassFormData, + confirmNewPassword: e.target.value, + }) + } + /> + +
-
-
- -
- - setForgotPassFormData({ - ...forgotPassFormData, - confirmNewPassword: e.target.value, - }) - } + -
-
-
- -
-
- - {t('backToHome')} + {t('backToLogin')}
- +
-
+ ); }; diff --git a/src/screens/LoginPage/LoginPage.module.css b/src/screens/LoginPage/LoginPage.module.css index b66db017d8..8e8e314ba9 100644 --- a/src/screens/LoginPage/LoginPage.module.css +++ b/src/screens/LoginPage/LoginPage.module.css @@ -16,13 +16,20 @@ } .row .right_portion { - height: 100vh; + min-height: 100vh; position: relative; overflow-y: scroll; + display: flex; + flex-direction: column; + justify-content: center; padding: 1rem 2.5rem; background: var(--bs-white); } +.row .right_portion::-webkit-scrollbar { + display: none; +} + .row .right_portion .langChangeBtn { margin: 0; position: absolute; @@ -35,6 +42,8 @@ width: 150px; display: block; margin: 1rem auto; + -webkit-animation: zoomIn 0.3s ease-in-out; + animation: zoomIn 0.3s ease-in-out; } .row .orText { @@ -83,7 +92,8 @@ height: 70px; width: unset; position: absolute; - top: 0.25rem; + margin: 0.5rem; + top: 0; right: 0; z-index: 100; } @@ -100,28 +110,62 @@ } .row .right_portion .langChangeBtn { - position: relative; - margin: 0; + position: absolute; + margin: 1rem; left: 0; top: 0; } + .marginTopForReg { + margin-top: 4rem !important; + } + .row .right_portion .talawa_logo { height: 120px; - margin: 0.75rem auto; + margin: 0 auto 2rem auto; } } + .active_tab { -webkit-animation: fadeIn 0.3s ease-in-out; animation: fadeIn 0.3s ease-in-out; } +@-webkit-keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes zoomIn { + 0% { + opacity: 0; + -webkit-transform: scale(0.5); + transform: scale(0.5); + } + + 100% { + opacity: 1; + -webkit-transform: scale(1); + transform: scale(1); + } +} + @-webkit-keyframes fadeIn { 0% { opacity: 0; -webkit-transform: translateY(2rem); transform: translateY(2rem); } + 100% { opacity: 1; -webkit-transform: translateY(0); @@ -135,6 +179,7 @@ -webkit-transform: translateY(2rem); transform: translateY(2rem); } + 100% { opacity: 1; -webkit-transform: translateY(0); diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index fe27ee9aed..a7f4029b92 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -228,7 +228,11 @@ function loginPage(): JSX.Element { - + {/* LOGIN FORM */}
input { cursor: pointer; color: #707070; } + .modalbody { width: 50px; } + .pluginStoreBtnContainer { display: flex; gap: 1rem; } + .greenregbtn { margin: 1rem 0 0; margin-top: 10px; @@ -200,6 +206,7 @@ form > input { width: 45%; height: 18px; } + .secondbtn { display: flex; align-items: center; diff --git a/src/screens/OrgList/OrgList.tsx b/src/screens/OrgList/OrgList.tsx index a34a084a14..d2229b2339 100644 --- a/src/screens/OrgList/OrgList.tsx +++ b/src/screens/OrgList/OrgList.tsx @@ -8,12 +8,15 @@ import { USER_ORGANIZATION_LIST, } from 'GraphQl/Queries/Queries'; import OrgListCard from 'components/OrgListCard/OrgListCard'; +import SuperAdminScreen from 'components/SuperAdminScreen/SuperAdminScreen'; import type { ChangeEvent } from 'react'; import React, { useEffect, useState } from 'react'; import { Col, Dropdown, Form, Row } from 'react-bootstrap'; import Button from 'react-bootstrap/Button'; import Modal from 'react-bootstrap/Modal'; import { useTranslation } from 'react-i18next'; +import InfiniteScroll from 'react-infinite-scroll-component'; +import { Link } from 'react-router-dom'; import { toast } from 'react-toastify'; import convertToBase64 from 'utils/convertToBase64'; import debounce from 'utils/debounce'; @@ -24,8 +27,6 @@ import type { InterfaceUserType, } from 'utils/interfaces'; import styles from './OrgList.module.css'; -import SuperAdminScreen from 'components/SuperAdminScreen/SuperAdminScreen'; -import { Link } from 'react-router-dom'; function orgList(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'orgList' }); @@ -48,6 +49,10 @@ function orgList(): JSX.Element { setdialogModalIsOpen(!dialogModalisOpen); document.title = t('title'); + const perPageResult = 8; + const [isLoading, setIsLoading] = useState(true); + const [hasMore, sethasMore] = useState(true); + const [isLoadingMore, setIsLoadingMore] = useState(false); const [searchByName, setSearchByName] = useState(''); const [showModal, setShowModal] = useState(false); const [formState, setFormState] = useState({ @@ -79,12 +84,19 @@ function orgList(): JSX.Element { loading, error: errorList, refetch: refetchOrgs, + fetchMore, }: { data: InterfaceOrgConnectionType | undefined; loading: boolean; error?: Error | undefined; refetch: any; + fetchMore: any; } = useQuery(ORGANIZATION_CONNECTION_LIST, { + variables: { + first: perPageResult, + skip: 0, + filter: searchByName, + }, notifyOnNetworkStatusChange: true, }); @@ -103,6 +115,14 @@ function orgList(): JSX.Element { }; }, []); + useEffect(() => { + if (loading && isLoadingMore == false) { + setIsLoading(true); + } else { + setIsLoading(false); + } + }, [loading]); + /* istanbul ignore next */ const isAdminForCurrentOrg = ( currentOrg: InterfaceOrgConnectionInfoType @@ -175,14 +195,66 @@ function orgList(): JSX.Element { window.location.assign('/'); } + /* istanbul ignore next */ + const resetAllParams = (): void => { + refetchOrgs({ + filter: '', + first: perPageResult, + skip: 0, + }); + sethasMore(true); + }; + + /* istanbul ignore next */ const handleSearchByName = (e: any): void => { const { value } = e.target; setSearchByName(value); + if (value == '') { + resetAllParams(); + return; + } refetchOrgs({ filter: value, }); }; + /* istanbul ignore next */ + const loadMoreOrganizations = (): void => { + console.log('loadMoreOrganizations'); + setIsLoadingMore(true); + fetchMore({ + variables: { + skip: orgsData?.organizationsConnection.length || 0, + }, + updateQuery: ( + prev: + | { organizationsConnection: InterfaceOrgConnectionType[] } + | undefined, + { + fetchMoreResult, + }: { + fetchMoreResult: + | { organizationsConnection: InterfaceOrgConnectionType[] } + | undefined; + } + ): + | { organizationsConnection: InterfaceOrgConnectionType[] } + | undefined => { + setIsLoadingMore(false); + if (!fetchMoreResult) return prev; + if (fetchMoreResult.organizationsConnection.length < perPageResult) { + sethasMore(false); + } + return { + organizationsConnection: [ + ...(prev?.organizationsConnection || []), + ...(fetchMoreResult.organizationsConnection || []), + ], + }; + }, + }); + }; + const debouncedHandleSearchByName = debounce(handleSearchByName); return ( <> @@ -244,8 +316,8 @@ function orgList(): JSX.Element { )}
- {/* Organizations List */} - {!loading && + {/* Text Infos for list */} + {!isLoading && ((orgsData?.organizationsConnection.length === 0 && searchByName.length == 0) || (userData && @@ -256,7 +328,7 @@ function orgList(): JSX.Element {

{t('noOrgErrorTitle')}

{t('noOrgErrorDescription')}
- ) : !loading && + ) : !isLoading && orgsData?.organizationsConnection.length == 0 && /* istanbul ignore next */ searchByName.length > 0 ? ( @@ -268,52 +340,87 @@ function orgList(): JSX.Element {
) : ( - <> - )} -
- {loading ? ( - <> - {[...Array(8)].map((_, index) => ( -
-
-
-
-
-
-
-
-
+ <> + + {[...Array(perPageResult)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+
+
-
-
-
- ))} - - ) : userData && userData.user.userType == 'SUPERADMIN' ? ( - orgsData?.organizationsConnection.map((item) => { - return ( -
- + ))} + + } + hasMore={hasMore} + className={styles.listBox} + data-testid="organizations-list" + endMessage={ +
+
{t('endOfResults')}
- ); - }) - ) : userData && - userData.user.userType == 'ADMIN' && - userData.user.adminFor.length > 0 ? ( - orgsData?.organizationsConnection.map((item) => { - if (isAdminForCurrentOrg(item)) { - return ( -
- -
- ); } - }) - ) : null} -
+ > + {isLoading ? ( + <> + {[...Array(perPageResult)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} + + ) : userData && userData.user.userType == 'SUPERADMIN' ? ( + orgsData?.organizationsConnection.map((item, index) => { + return ( +
+ +
+ ); + }) + ) : ( + userData && + userData.user.userType == 'ADMIN' && + userData.user.adminFor.length > 0 && + orgsData?.organizationsConnection.map((item) => { + if (isAdminForCurrentOrg(item)) { + return ( +
+ +
+ ); + } + }) + )} + + + )} {/* Create Organization Modal */} {' '} + {/* Plugin Notification Modal after Org is Created */}
@@ -502,10 +610,8 @@ function orgList(): JSX.Element {
- {/* Plugin Notification after Org is Created */} ); } - export default orgList; diff --git a/src/screens/OrgList/OrgListMocks.ts b/src/screens/OrgList/OrgListMocks.ts index 419af22036..1a1ab30ab7 100644 --- a/src/screens/OrgList/OrgListMocks.ts +++ b/src/screens/OrgList/OrgListMocks.ts @@ -34,7 +34,7 @@ const organizations: InterfaceOrgConnectionInfoType[] = [ _id: '1', creator: { _id: 'xyz', firstName: 'John', lastName: 'Doe' }, image: '', - name: 'Akatsuki', + name: 'Palisadoes Foundation', createdAt: '02/02/2022', admins: [ { @@ -46,7 +46,7 @@ const organizations: InterfaceOrgConnectionInfoType[] = [ _id: '234', }, ], - location: 'Washington DC', + location: 'Jamaica', }, ]; @@ -80,6 +80,12 @@ const MOCKS = [ { request: { query: ORGANIZATION_CONNECTION_LIST, + variables: { + first: 8, + skip: 0, + filter: '', + }, + notifyOnNetworkStatusChange: true, }, result: { data: { @@ -101,6 +107,12 @@ const MOCKS_EMPTY = [ { request: { query: ORGANIZATION_CONNECTION_LIST, + variables: { + first: 8, + skip: 0, + filter: '', + }, + notifyOnNetworkStatusChange: true, }, result: { data: { @@ -124,6 +136,12 @@ const MOCKS_ADMIN = [ { request: { query: ORGANIZATION_CONNECTION_LIST, + variables: { + first: 8, + skip: 0, + filter: '', + }, + notifyOnNetworkStatusChange: true, }, result: { data: { @@ -142,4 +160,4 @@ const MOCKS_ADMIN = [ }, ]; -export { MOCKS, MOCKS_EMPTY, MOCKS_ADMIN }; +export { MOCKS, MOCKS_ADMIN, MOCKS_EMPTY }; diff --git a/src/screens/OrgPost/OrgPost.module.css b/src/screens/OrgPost/OrgPost.module.css index 037e5f40c8..f9ff4a0794 100644 --- a/src/screens/OrgPost/OrgPost.module.css +++ b/src/screens/OrgPost/OrgPost.module.css @@ -1,85 +1,51 @@ -.navbarbg { - height: 60px; - background-color: white; +.mainpage { display: flex; - margin-bottom: 30px; - z-index: 1; - position: relative; flex-direction: row; - justify-content: space-between; - box-shadow: 0px 0px 8px 2px #c8c8c8; } - -.checkboxdiv { - position: relative; +.btnsContainer { display: flex; + margin: 2.5rem 0 2.5rem 0; } -.checkboxdiv label { - margin-right: 4px; -} - -.checkboxdiv input { - margin-right: 10px; +.btnsContainer .btnsBlock { + display: flex; } -.logo { - color: #707070; - margin-left: 0; +.btnsContainer .btnsBlock button { + margin-left: 1rem; display: flex; + justify-content: center; align-items: center; - text-decoration: none; } -.logo img { - margin-top: 0px; - margin-left: 10px; - height: 64px; - width: 70px; +.btnsContainer .input { + flex: 1; + position: relative; } -.logo > strong { - line-height: 1.5rem; - margin-left: -5px; - font-family: sans-serif; - font-size: 19px; - color: #707070; -} -.mainpage { - display: flex; - flex-direction: row; +.btnsContainer input { + outline: 1px solid var(--bs-gray-400); } -.sidebar { - z-index: 0; - padding-top: 5px; - margin: 0; - height: 100%; -} -.sidebar:after { - content: ''; - background-color: #f7f7f7; - position: absolute; - width: 2px; - height: 600px; - top: 10px; - left: 94%; - display: block; + +.btnsContainer .input button { + width: 52px; } -.sidebarsticky { - padding-left: 45px; - margin-top: 7px; + +.preview { + display: flex; + position: relative; + width: 100%; + margin-top: 10px; + justify-content: center; } -.sidebarsticky > p { - margin-top: -10px; +.preview img { + width: 400px; + height: auto; } - -.navitem { - padding-left: 27%; - padding-top: 12px; - padding-bottom: 12px; - cursor: pointer; +.preview video { + width: 400px; + height: auto; } - .logintitle { color: #707070; font-weight: 600; @@ -89,16 +55,6 @@ border-bottom: 3px solid #31bb6b; width: 15%; } -.searchtitle { - color: #707070; - font-weight: 600; - font-size: 18px; - margin-bottom: 20px; - line-height: 25px; - padding-bottom: 5px; - border-bottom: 3px solid #31bb6b; - width: 60%; -} .logintitleadmin { color: #707070; font-weight: 600; @@ -128,10 +84,69 @@ .justifysp { display: flex; justify-content: space-between; + align-items: center; + margin-bottom: 3rem; +} + +@media (max-width: 1120px) { + .contract { + padding-left: calc(250px + 2rem + 1.5rem); + } + + .listBox .itemCard { + width: 100%; + } +} + +@media (max-width: 1020px) { + .btnsContainer { + flex-direction: column; + margin: 1.5rem 0; + } + + .btnsContainer .btnsBlock { + margin: 1.5rem 0 0 0; + justify-content: space-between; + } + + .btnsContainer .btnsBlock button { + margin: 0; + } + + .btnsContainer .btnsBlock div button { + margin-right: 1.5rem; + } +} + +/* For mobile devices */ + +@media (max-width: 520px) { + .btnsContainer { + margin-bottom: 0; + } + + .btnsContainer .btnsBlock { + display: block; + margin-top: 1rem; + margin-right: 0; + } + + .btnsContainer .btnsBlock div { + flex: 1; + } + + .btnsContainer .btnsBlock div[title='Sort organizations'] { + margin-right: 0.5rem; + } + + .btnsContainer .btnsBlock button { + margin-bottom: 1rem; + margin-right: 0; + width: 100%; + } } @media screen and (max-width: 575.5px) { .justifysp { - padding-left: 55px; display: flex; justify-content: space-between; width: 100%; @@ -147,6 +162,7 @@ border-radius: 5px; font-size: 16px; height: 60%; + width: 60%; color: white; outline: none; font-weight: 600; @@ -159,7 +175,6 @@ justify-content: space-between; border: none; } - .form_wrapper { margin-top: 27px; top: 50%; @@ -216,18 +231,19 @@ .modalbody { width: 50px; } -.greenregbtn { - border: 1px solid #e8e5e5; - box-shadow: 0 2px 2px #e8e5e5; - border-radius: 5px; - background-color: #31bb6b; - font-size: 16px; - height: 60%; - color: white; - outline: none; + +.closeButton { + position: absolute; + top: 0px; + right: 0px; + background: transparent; + transform: scale(1.2); + cursor: pointer; + border: none; + color: #707070; font-weight: 600; + font-size: 16px; cursor: pointer; - transition: transform 0.2s, box-shadow 0.2s; } .sidebarsticky > input { text-decoration: none; @@ -249,7 +265,9 @@ box-shadow: 0 0 5pt 0.5pt #d3d3d3; outline: none; } - +button[data-testid='createPostBtn'] { + display: block; +} .loader, .loader:after { border-radius: 50%; diff --git a/src/screens/OrgPost/OrgPost.test.tsx b/src/screens/OrgPost/OrgPost.test.tsx index 1db08a4179..93f603d446 100644 --- a/src/screens/OrgPost/OrgPost.test.tsx +++ b/src/screens/OrgPost/OrgPost.test.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { MockedProvider } from '@apollo/react-testing'; import { BrowserRouter } from 'react-router-dom'; -import { act, render, screen } from '@testing-library/react'; +import { act, render, screen, fireEvent } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Provider } from 'react-redux'; import 'jest-location-mock'; @@ -14,6 +14,7 @@ import { CREATE_POST_MUTATION } from 'GraphQl/Mutations/mutations'; import i18nForTest from 'utils/i18nForTest'; import { StaticMockLink } from 'utils/StaticMockLink'; import { ToastContainer } from 'react-toastify'; +import { debug } from 'jest-preview'; const MOCKS = [ { @@ -35,6 +36,7 @@ const MOCKS = [ text: 'THis is the frist post', imageUrl: null, videoUrl: null, + createdAt: '2023-08-24T09:26:56.524+00:00', creator: { _id: '640d98d9eb6a743d75341067', firstName: 'Aditya', @@ -44,6 +46,7 @@ const MOCKS = [ likeCount: 0, commentCount: 0, comments: [], + pinned: false, likedBy: [], }, { @@ -52,6 +55,7 @@ const MOCKS = [ text: 'THis is the post two', imageUrl: null, videoUrl: null, + createdAt: '2023-08-24T09:26:56.524+00:00', creator: { _id: '640d98d9eb6a743d75341067', firstName: 'Aditya', @@ -60,6 +64,7 @@ const MOCKS = [ }, likeCount: 0, commentCount: 0, + pinned: false, likedBy: [], comments: [], }, @@ -104,7 +109,6 @@ const MOCKS = [ }, ]; const link = new StaticMockLink(MOCKS, true); -const link2 = new StaticMockLink([], true); async function wait(ms = 500): Promise { await act(() => { @@ -119,12 +123,7 @@ describe('Organisation Post Page', () => { posttitle: 'dummy post', postinfo: 'This is a dummy post', postImage: new File(['hello'], 'hello.png', { type: 'image/png' }), - }; - - const formDataEmpty = { - posttitle: ' ', - postinfo: ' ', - postImage: new File(['hello'], 'hello.png', { type: 'image/png' }), + postVideo: new File(['hello'], 'hello.mp4', { type: 'video/mp4' }), }; test('correct mock data should be queried', async () => { @@ -137,6 +136,7 @@ describe('Organisation Post Page', () => { text: 'THis is the frist post', imageUrl: null, videoUrl: null, + createdAt: '2023-08-24T09:26:56.524+00:00', creator: { _id: '640d98d9eb6a743d75341067', firstName: 'Aditya', @@ -145,13 +145,14 @@ describe('Organisation Post Page', () => { }, likeCount: 0, commentCount: 0, - comments: [], + pinned: false, likedBy: [], + comments: [], }); }); - test('should render props and text elements test for the screen', async () => { - const { container } = render( + test('Testing create post functionality', async () => { + render( @@ -163,17 +164,31 @@ describe('Organisation Post Page', () => { ); - expect(container.textContent).not.toBe('Loading data...'); + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + userEvent.type(screen.getByTestId('modalTitle'), formData.posttitle); + + userEvent.type(screen.getByTestId('modalinfo'), formData.postinfo); + userEvent.upload( + screen.getByTestId('organisationImage'), + formData.postImage + ); + userEvent.upload( + screen.getByTestId('organisationImage'), + formData.postVideo + ); + + userEvent.click(screen.getByTestId('createPostBtn')); await wait(); - expect(container.textContent).toMatch('Search Post'); - expect(container.textContent).toMatch('Posts'); - expect(container.textContent).toMatch('+ Create Post'); - }); - // Test : Render two radio buttons - test('should render two radio buttons', async () => { - const { container } = render( + userEvent.click(screen.getByTestId('closeOrganizationModal')); + }, 15000); + + test('Testing search functionality', async () => { + render( @@ -184,21 +199,123 @@ describe('Organisation Post Page', () => { ); + async function debounceWait(ms = 200): Promise { + await act(() => { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + }); + } + await debounceWait(); + userEvent.type(screen.getByPlaceholderText(/Search By/i), 'postone'); + await debounceWait(); + const sortDropdown = screen.getByTestId('sort'); + userEvent.click(sortDropdown); + }); + test('Testing search text and title toggle', async () => { + await act(async () => { + // Wrap the test code in act + render( + + + + + + + + + + ); + + await wait(); + + const searchInput = screen.getByTestId('searchByName'); + expect(searchInput).toHaveAttribute('placeholder', 'Search By Title'); + + const inputText = screen.getByTestId('searchBy'); + + fireEvent.click(inputText); + const toggleText = screen.getByTestId('Text'); + + fireEvent.click(toggleText); + + expect(searchInput).toHaveAttribute('placeholder', 'Search By Text'); + fireEvent.click(inputText); + const toggleTite = screen.getByTestId('searchTitle'); + fireEvent.click(toggleTite); + expect(searchInput).toHaveAttribute('placeholder', 'Search By Title'); + }); + }); + test('Testing search latest and oldest toggle', async () => { + await act(async () => { + // Wrap the test code in act + render( + + + + + + + + + + ); + + await wait(); + + const searchInput = screen.getByTestId('sort'); + expect(searchInput).toBeInTheDocument(); + + const inputText = screen.getByTestId('sortpost'); + + fireEvent.click(inputText); + const toggleText = screen.getByTestId('latest'); + + fireEvent.click(toggleText); + + expect(searchInput).toBeInTheDocument(); + fireEvent.click(inputText); + const toggleTite = screen.getByTestId('oldest'); + fireEvent.click(toggleTite); + expect(searchInput).toBeInTheDocument(); + }); + }); + test('After creating a post, the data should be refetched', async () => { + const refetchMock = jest.fn(); - expect(container.textContent).not.toBe('Loading data...'); + render( + + + + + + + + + + + ); await wait(); - expect(container.textContent).toMatch('Title'); - expect(container.textContent).toMatch('Text'); + userEvent.click(screen.getByTestId('createPostModalBtn')); + + // Fill in post form fields... + + userEvent.click(screen.getByTestId('createPostBtn')); + + await wait(); + + expect(refetchMock).toHaveBeenCalledTimes(0); }); - test('Testing create post functionality', async () => { + test('Create post without media', async () => { render( + @@ -207,29 +324,22 @@ describe('Organisation Post Page', () => { ); await wait(); - userEvent.click(screen.getByTestId('createPostModalBtn')); - userEvent.type( - screen.getByPlaceholderText(/Post Title/i), - formData.posttitle - ); - - userEvent.type( - screen.getByPlaceholderText(/What do you to talk about?/i), - formData.postinfo - ); - userEvent.upload(screen.getByLabelText(/Post Image:/i), formData.postImage); + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); - userEvent.click(screen.getByTestId('createPostBtn')); - - await wait(); + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); - userEvent.click(screen.getByTestId('closePostModalBtn')); + const createPostBtn = screen.getByTestId('createPostBtn'); + fireEvent.click(createPostBtn); }, 15000); - test('Create Post should throw error when empty strings have been entered', async () => { - const { container } = render( + test('Create post and preview', async () => { + render( @@ -243,65 +353,121 @@ describe('Organisation Post Page', () => { ); await wait(); - userEvent.click(screen.getByTestId('createPostModalBtn')); - userEvent.type( - screen.getByPlaceholderText(/Post Title/i), - formDataEmpty.posttitle - ); + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); - userEvent.type( - screen.getByPlaceholderText(/What do you to talk about?/i), - formDataEmpty.postinfo - ); + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); + const file = new File(['image content'], 'image.png', { + type: 'image/png', + }); + const input = screen.getByTestId('organisationImage'); + userEvent.upload(input, file); - userEvent.upload( - screen.getByLabelText(/Post Image:/i), - formDataEmpty.postImage + await screen.findByAltText('Post Image Preview'); + expect(screen.getByAltText('Post Image Preview')).toBeInTheDocument(); + + const createPostBtn = screen.getByTestId('createPostBtn'); + fireEvent.click(createPostBtn); + debug(); + }, 15000); + + test('Modal opens and closes', async () => { + render( + + + + + + + + + ); - userEvent.click(screen.getByTestId('createPostBtn')); + await wait(); + + const createPostModalBtn = screen.getByTestId('createPostModalBtn'); + + userEvent.click(createPostModalBtn); + + const modalTitle = screen.getByTestId('modalOrganizationHeader'); + expect(modalTitle).toBeInTheDocument(); + + const closeButton = screen.getByTestId('closeOrganizationModal'); + userEvent.click(closeButton); await wait(); - expect(container.textContent).toMatch( - 'Text fields cannot be empty strings' + const closedModalTitle = screen.queryByText(/postDetail/i); + expect(closedModalTitle).not.toBeInTheDocument(); + }); + it('renders the form with input fields and buttons', async () => { + render( + + + + + + + + + + ); - }, 15000); - test('Testing search functionality', async () => { + await wait(); + userEvent.click(screen.getByTestId('createPostModalBtn')); + + // Check if input fields and buttons are present + expect(screen.getByTestId('modalTitle')).toBeInTheDocument(); + expect(screen.getByTestId('modalinfo')).toBeInTheDocument(); + expect(screen.getByTestId('organisationImage')).toBeInTheDocument(); + expect(screen.getByTestId('organisationVideo')).toBeInTheDocument(); + expect(screen.getByTestId('createPostBtn')).toBeInTheDocument(); + }); + + it('allows users to input data into the form fields', async () => { render( + ); - async function debounceWait(ms = 200): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); - } - await debounceWait(); - userEvent.type(screen.getByPlaceholderText(/Search By/i), 'postone'); - await debounceWait(); - }); - test('Testing when post data is not present', async () => { - window.location.assign('/orglist'); + await wait(); + userEvent.click(screen.getByTestId('createPostModalBtn')); + // Simulate user input + fireEvent.change(screen.getByTestId('modalTitle'), { + target: { value: 'Test Title' }, + }); + fireEvent.change(screen.getByTestId('modalinfo'), { + target: { value: 'Test Info' }, + }); + + // Check if input values are set correctly + expect(screen.getByTestId('modalTitle')).toHaveValue('Test Title'); + expect(screen.getByTestId('modalinfo')).toHaveValue('Test Info'); + }); + + test('allows users to upload an image', async () => { render( - + + @@ -310,6 +476,73 @@ describe('Organisation Post Page', () => { ); await wait(); - expect(window.location).toBeAt('/orglist'); + userEvent.click(screen.getByTestId('createPostModalBtn')); + + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); + + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); + const file = new File(['image content'], 'image.png', { + type: 'image/png', + }); + const input = screen.getByTestId('organisationImage'); + userEvent.upload(input, file); + + await screen.findByAltText('Post Image Preview'); + expect(screen.getByAltText('Post Image Preview')).toBeInTheDocument(); + + const closeButton = screen.getByTestId('closePreview'); + fireEvent.click(closeButton); + }, 15000); + test('Create post, preview image, and close preview', async () => { + await act(async () => { + render( + + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('createPostModalBtn')); + + const postTitleInput = screen.getByTestId('modalTitle'); + fireEvent.change(postTitleInput, { target: { value: 'Test Post' } }); + + const postInfoTextarea = screen.getByTestId('modalinfo'); + fireEvent.change(postInfoTextarea, { + target: { value: 'Test post information' }, + }); + + const videoFile = new File(['video content'], 'video.mp4', { + type: 'video/mp4', + }); + + const videoInput = screen.getByTestId('organisationVideo'); + fireEvent.change(videoInput, { + target: { + files: [videoFile], + }, + }); + + // Check if the video is displayed + const videoPreview = await screen.findByTestId('videoPreview'); + expect(videoPreview).toBeInTheDocument(); + + // Check if the close button for the video works + const closeVideoPreviewButton = screen.getByTestId('videoclosebutton'); + fireEvent.click(closeVideoPreviewButton); + expect(videoPreview).not.toBeInTheDocument(); + }); }); }); diff --git a/src/screens/OrgPost/OrgPost.tsx b/src/screens/OrgPost/OrgPost.tsx index 2e897aede8..9f098c291b 100644 --- a/src/screens/OrgPost/OrgPost.tsx +++ b/src/screens/OrgPost/OrgPost.tsx @@ -1,27 +1,38 @@ import type { ChangeEvent } from 'react'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; +import { Search } from '@mui/icons-material'; +import SortIcon from '@mui/icons-material/Sort'; import Row from 'react-bootstrap/Row'; -import Col from 'react-bootstrap/Col'; import Modal from 'react-bootstrap/Modal'; import { Form } from 'react-bootstrap'; import { useMutation, useQuery } from '@apollo/client'; import Button from 'react-bootstrap/Button'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; - +import Dropdown from 'react-bootstrap/Dropdown'; import styles from './OrgPost.module.css'; import OrgPostCard from 'components/OrgPostCard/OrgPostCard'; import { ORGANIZATION_POST_CONNECTION_LIST } from 'GraphQl/Queries/Queries'; import { CREATE_POST_MUTATION } from 'GraphQl/Mutations/mutations'; -import PaginationList from 'components/PaginationList/PaginationList'; import debounce from 'utils/debounce'; import convertToBase64 from 'utils/convertToBase64'; import NotFound from 'components/NotFound/NotFound'; -import { Form as StyleBox } from 'react-bootstrap'; import { errorHandler } from 'utils/errorHandler'; import Loader from 'components/Loader/Loader'; import OrganizationScreen from 'components/OrganizationScreen/OrganizationScreen'; +interface InterfaceOrgPost { + _id: string; + title: string; + text: string; + imageUrl: string; + videoUrl: string; + organizationId: string; + creator: { firstName: string; lastName: string }; + pinned: boolean; + createdAt: string; +} + function orgPost(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'orgPost', @@ -33,15 +44,11 @@ function orgPost(): JSX.Element { posttitle: '', postinfo: '', postImage: '', + postVideo: '', }); + const [sortingOption, setSortingOption] = useState('latest'); const [showTitle, setShowTitle] = useState(true); - const searchChange = (ev: any): void => { - setShowTitle(ev.target.value === 'searchTitle'); - }; - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(5); - const currentUrl = window.location.href.split('=')[1]; const showInviteModal = (): void => { @@ -49,6 +56,12 @@ function orgPost(): JSX.Element { }; const hideInviteModal = (): void => { setPostModalIsOpen(false); + setPostFormState({ + posttitle: '', + postinfo: '', + postImage: '', + postVideo: '', + }); }; const { @@ -61,7 +74,19 @@ function orgPost(): JSX.Element { }); const [create, { loading: createPostLoading }] = useMutation(CREATE_POST_MUTATION); + const [displayedPosts, setDisplayedPosts] = useState( + orgPostListData?.postsByOrganizationConnection.edges || [] + ); + useEffect(() => { + if (orgPostListData && orgPostListData.postsByOrganizationConnection) { + const newDisplayedPosts = sortPosts( + orgPostListData.postsByOrganizationConnection.edges, + sortingOption + ); + setDisplayedPosts(newDisplayedPosts); + } + }, [orgPostListData, sortingOption]); const createPost = async (e: ChangeEvent): Promise => { e.preventDefault(); @@ -69,6 +94,7 @@ function orgPost(): JSX.Element { posttitle: _posttitle, postinfo: _postinfo, postImage, + postVideo, } = postformState; const posttitle = _posttitle.trim(); @@ -84,7 +110,7 @@ function orgPost(): JSX.Element { title: posttitle, text: postinfo, organizationId: currentUrl, - file: postImage, + file: postImage || postVideo, }, }); /* istanbul ignore next */ @@ -95,6 +121,7 @@ function orgPost(): JSX.Element { posttitle: '', postinfo: '', postImage: '', + postVideo: '', }); setPostModalIsOpen(false); // close the modal } @@ -113,21 +140,6 @@ function orgPost(): JSX.Element { window.location.assign('/orglist'); } - /* istanbul ignore next */ - const handleChangePage = ( - event: React.MouseEvent | null, - newPage: number - ): void => { - setPage(newPage); - }; - /* istanbul ignore next */ - const handleChangeRowsPerPage = ( - event: React.ChangeEvent - ): void => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - const handleSearch = (e: any): void => { const { value } = e.target; const filterData = { @@ -140,143 +152,197 @@ function orgPost(): JSX.Element { const debouncedHandleSearch = debounce(handleSearch); + const handleSorting = (option: string): void => { + setSortingOption(option); + }; + + const sortPosts = ( + posts: InterfaceOrgPost[], + sortingOption: string + ): InterfaceOrgPost[] => { + const sortedPosts = [...posts]; + + if (sortingOption === 'latest') { + sortedPosts.sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + } else if (sortingOption === 'oldest') { + sortedPosts.sort( + (a, b) => + new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() + ); + } + + return sortedPosts; + }; + + const sortedPostsList: InterfaceOrgPost[] = [...displayedPosts]; + sortedPostsList.sort((a: InterfaceOrgPost, b: InterfaceOrgPost) => { + if (a.pinned === b.pinned) { + return 0; + } + + if (a.pinned) { + return -1; + } + return 1; + }); return ( <> - - -
-
-
{t('searchPost')}
-
-
- - -
-
+ +
+
+
+
-
- - -
- -

{t('posts')}

+
+
+ + +
+ - -
- {orgPostListData && - orgPostListData.postsByOrganizationConnection.edges.length > - 0 ? ( - (rowsPerPage > 0 - ? orgPostListData.postsByOrganizationConnection.edges.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - : rowsPerPage > 0 - ? orgPostListData.postsByOrganizationConnection.edges.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - : orgPostListData.postsByOrganizationConnection.edges - ).map( - (datas: { - _id: string; - title: string; - text: string; - imageUrl: string; - videoUrl: string; - organizationId: string; - creator: { firstName: string; lastName: string }; - }) => { - return ( - - ); - } - ) - ) : ( - - )}
-
- - - - + {sortedPostsList && sortedPostsList.length > 0 ? ( + sortedPostsList.map( + (datas: { + _id: string; + title: string; + text: string; + imageUrl: string; + videoUrl: string; + organizationId: string; + creator: { firstName: string; lastName: string }; + pinned: boolean; + }) => ( + - - -
+ ) + ) + ) : ( + + )}
- +
- - -

{t('postDetails')}

- + + + {t('postDetails')} - -
- + + + {t('postTitle')} - -