diff --git a/.github/workflows/compare_translations.py b/.github/workflows/compare_translations.py new file mode 100644 index 0000000000..f6f22e843c --- /dev/null +++ b/.github/workflows/compare_translations.py @@ -0,0 +1,170 @@ +"""Script to encourage more efficient coding practices. +Methodology: + + Utility for comparing translations between default and other languages. + + This module defines a function to compare two translations + and print any missing keys in the other language's translation. +Attributes: + + FileTranslation : Named tuple to represent a combination + of file and missing translations. + + Fields: + - file (str): The file name. + - missing_translations (list): List of missing translations. + +Functions: + compare_translations(default_translation, other_translation): + Compare two translations and print missing keys. + + load_translation(filepath): + Load translation from a file. + + check_translations(): + Load the default translation and compare it with other translations. + + main(): + The main function to run the script. + Parses command-line arguments, checks for the + existence of the specified directory, and then + calls check_translations with the provided or default directory. + + +Usage: + This script can be executed to check and print missing + translations in other languages based on the default English translation. + +Example: + python compare_translations.py +NOTE: + This script complies with our python3 coding and documentation standards + and should be used as a reference guide. It complies with: + + 1) Pylint + 2) Pydocstyle + 3) Pycodestyle + 4) Flake8 + +""" +# standard imports +import argparse +import json +import os +import sys +from collections import namedtuple + +# Named tuple for file and missing +# translations combination +FileTranslation = namedtuple("FileTranslation", + ["file", "missing_translations"]) + + +def compare_translations(default_translation, + other_translation, default_file, other_file): + """Compare two translations and return detailed info about missing/mismatched keys. + + Args: + default_translation (dict): The default translation (en.json). + other_translation (dict): The other language translation. + default_file (str): The name of the default translation file. + other_file (str): The name of the other + translation file. + + Returns: + list: A list of detailed error messages for each missing/mismatched key. + """ + errors = [] + + # Check for missing keys in other_translation + for key in default_translation: + if key not in other_translation: + error_msg = f"Missing Key: '{key}' - This key from '{default_file}' is missing in '{other_file}'." + errors.append(error_msg) + # Check for keys in other_translation that don't match any in default_translation + for key in other_translation: + if key not in default_translation: + error_msg = f"Error Key: '{key}' - This key in '{other_file}' does not match any key in '{default_file}'." + errors.append(error_msg) + return errors + + +def load_translation(filepath): + """Load translation from a file. + + Args: + filepath: Path to the translation file + + Returns: + translation: Loaded translation + """ + with open(filepath, "r", encoding="utf-8") as file: + translation = json.load(file) + return translation + + +def check_translations(directory): + """Load default translation and compare with other translations. + + Args: + directory (str): The directory containing translation files. + + Returns: + None + """ + default_file = "en.json" + default_translation = load_translation(os.path.join(directory, default_file)) + translations = os.listdir(directory) + translations.remove(default_file) # Exclude default translation + + error_found = False + + for translation_file in translations: + other_file = os.path.join(directory, translation_file) + other_translation = load_translation(other_file) + + # Compare translations and get detailed error messages + errors = compare_translations( + default_translation, other_translation, default_file, translation_file + ) + if errors: + error_found = True + print(f"File {translation_file} has missing translations for:") + for error in errors: + print(f" - {error}") + + if error_found: + sys.exit(1) # Exit with an error status code + else: + print("All translations are present") + sys.exit(0) + + +def main(): + """ + + Parse command-line arguments, check for the existence of the specified directory + and call check_translations with the provided or default directory. + + """ + parser = argparse.ArgumentParser( + description="Check and print missing translations for all non-default languages." + ) + parser.add_argument( + "--directory", + type=str, + nargs="?", + default=os.path.join(os.getcwd(), "public/locales"), + help="Directory containing translation files(relative to the root directory).", + ) + args = parser.parse_args() + + if not os.path.exists(args.directory): + print(f"Error: The specified directory '{args.directory}' does not exist.") + sys.exit(1) + + check_translations(args.directory) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 33b810655b..4f58b904f9 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -20,10 +20,8 @@ env: CODECOV_UNIQUE_NAME: CODECOV_UNIQUE_NAME-${{ github.run_id }}-${{ github.run_number }} jobs: - - Continuous-Integration: - - name: Performs linting, formatting, type-checking, and testing on the application + Code-Quality-Checks: + name: Performs linting, formatting, type-checking runs-on: ubuntu-latest steps: - name: Checkout the Repository @@ -49,14 +47,35 @@ jobs: if: steps.changed-files.outputs.only_changed != 'true' run: npm run typecheck + - name: Run linting check + if: steps.changed-files.outputs.only_changed != 'true' + run: npm run lint:check + + - name: Compare translation files + run: | + chmod +x .github/workflows/compare_translations.py + python .github/workflows/compare_translations.py --directory public/locales + + + Test-Application: + name: Test Application + runs-on: ubuntu-latest + needs: [Code-Quality-Checks] + steps: + - name: Checkout the Repository + uses: actions/checkout@v3 + + - name: Install Dependencies + run: npm install --legacy-peer-deps + + - name: Get changed TypeScript files + id: changed-files + uses: tj-actions/changed-files@v40 + - name: Run tests if: steps.changed-files.outputs.only_changed != 'true' run: npm run test -- --watchAll=false --coverage - - name: Run linting check - if: steps.changed-files.outputs.only_changed != 'true' - run: npm run lint:check - - name: TypeScript compilation for changed files run: | for file in ${{ steps.changed-files.outputs.all_files }}; do @@ -88,7 +107,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v3 with: - node-version: '16.14.1' + node-version: '20.x' - name: resolve dependency run: npm install -g @graphql-inspector/cli diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 7b9948c184..e76917b63e 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -22,6 +22,9 @@ env: jobs: Code-Coverage: runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.x] steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 diff --git a/.husky/pre-commit b/.husky/pre-commit index e4debee2be..4dc4624cb1 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -4,5 +4,6 @@ npm run format:fix npm run lint:fix npm run typecheck +npm run update:toc git add . diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 4a38c46419..b82fab3779 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,23 @@ # Contributor Covenant Code of Conduct +# Table of Contents + + + +- [Our Pledge](#our-pledge) +- [Our Standards](#our-standards) +- [Enforcement Responsibilities](#enforcement-responsibilities) +- [Scope](#scope) +- [Enforcement](#enforcement) +- [Enforcement Guidelines](#enforcement-guidelines) + - [1. Correction](#1-correction) + - [2. Warning](#2-warning) + - [3. Temporary Ban](#3-temporary-ban) + - [4. Permanent Ban](#4-permanent-ban) +- [Attribution](#attribution) + + + ## Our Pledge We as members, contributors, and leaders pledge to make participation in our diff --git a/CODE_STYLE.md b/CODE_STYLE.md index b93a8c0f57..7e7f69b40f 100644 --- a/CODE_STYLE.md +++ b/CODE_STYLE.md @@ -1,4 +1,3 @@ - # Talawa Admin Code Style For Talawa Admin, most of the rules for the code style have been enforced with ESLint, but this document serves to provide an overview of the Code style used in Talawa Admin and the Rationale behind it. @@ -7,6 +6,21 @@ The code style must be strictly adhered to, to ensure that there is consistency code style should not be changed and must be followed. +# Table of Contents + + + +- [Tech Stack](#tech-stack) +- [Component Structure](#component-structure) +- [Code Style and Naming Conventions](#code-style-and-naming-conventions) +- [Test and Code Linting](#test-and-code-linting) +- [Folder/Directory Structure](#folderdirectory-structure) + - [Sub Directories of `src`](#sub-directories-of-src) +- [Imports](#imports) +- [Customising Bootstrap](#customising-bootstrap) + + + ## Tech Stack - Typescript diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d3138a981..acb7d3b038 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,18 +6,20 @@ If you are new to contributing to open source, please read the Open Source Guide ## Table of Contents -- [Contributing to Talawa-Admin](#contributing-to-talawa-admin) - - [Table of Contents](#table-of-contents) - - [Code of Conduct](#code-of-conduct) - - [Ways to Contribute](#ways-to-contribute) - - [Our Development Process](#our-development-process) - - [Issues](#issues) - - [Pull Requests](#pull-requests) - - [Branching Strategy](#branching-strategy) - - [Conflict Resolution](#conflict-resolution) - - [Contributing Code](#contributing-code) - - [Internships](#internships) - - [Community](#community) + + +- [Code of Conduct](#code-of-conduct) +- [Ways to Contribute](#ways-to-contribute) + - [Our Development Process](#our-development-process) + - [Issues](#issues) + - [Pull Requests](#pull-requests) + - [Branching Strategy](#branching-strategy) + - [Conflict Resolution](#conflict-resolution) + - [Contributing Code](#contributing-code) +- [Internships](#internships) +- [Community](#community) + + ## Code of Conduct diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index aecf8cc132..7691b5d452 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1,5 +1,16 @@ # Documentation Welcome to our documentation guide. Here are some useful tips you need to know! + +# Table of Contents + + + +- [Where to find our documentation](#where-to-find-our-documentation) +- [How to use Docusaurus](#how-to-use-docusaurus) +- [Other information](#other-information) + + + ## Where to find our documentation Our documentation can be found in ONLY TWO PLACES: diff --git a/INSTALLATION.md b/INSTALLATION.md index c0e87d601b..a628a75d28 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -4,18 +4,18 @@ This document provides instructions on how to set up and start a running instanc # Table of Contents -- [Talawa-Admin Installation](#talawa-admin-installation) -- [Table of Contents](#table-of-contents) + + - [Prerequisites for Developers](#prerequisites-for-developers) - [Installation](#installation) - - [Clone This Repository](#clone-this-repository) - - [Change Directory into the Cloned Repo](#change-directory-into-the-cloned-repo) - - [Setting up NPM](#setting-up-npm) + - [Setting up this repository](#setting-up-this-repository) + - [Setting up npm](#setting-up-npm) + - [Setting up Typescript](#setting-up-typescript) - [Installing required packages/dependencies](#installing-required-packagesdependencies) - [Configuration](#configuration) - [Creating .env file](#creating-env-file) - - [Setting up REACT\_APP\_TALAWA\_URL in .env file](#setting-up-react_app_talawa_url-in-env-file) - - [Setting up REACT\_APP\_RECAPTCHA\_SITE\_KEY in .env file](#setting-up-react_app_recaptcha_site_key-in-env-file) + - [Setting up REACT_APP_TALAWA_URL in .env file](#setting-up-react_app_talawa_url-in-env-file) + - [Setting up REACT_APP_RECAPTCHA_SITE_KEY in .env file](#setting-up-react_app_recaptcha_site_key-in-env-file) - [Post Configuration Steps](#post-configuration-steps) - [Running Talawa-Admin](#running-talawa-admin) - [Accessing Talawa-Admin](#accessing-talawa-admin) @@ -29,6 +29,8 @@ This document provides instructions on how to set up and start a running instanc - [pre-commit hook](#pre-commit-hook) - [post-merge hook](#post-merge-hook) + + # Prerequisites for Developers We recommend that you follow these steps before beginning development work on Talawa-Admin: @@ -39,30 +41,41 @@ We recommend that you follow these steps before beginning development work on Ta The INSTALLATION.md files in both repositories show you how. The Talawa-API INSTALLATION.md will also show you the Organization URL to use access Talawa Admin. # Installation + You will need to have copies of your code on your local system. Here's how to do that. -## Clone This Repository + +## Setting up this repository First you need a local copy of `talawa-admin`. Run the following command in the directory of choice on your local system. +1. Navigate to the folder where you want to setup the repository. Here, I will set it up in a folder called `talawa`. +2. Navigate to the folder and open a terminal in this folder (you can right-click and choose appropiate option based onn your OS). Next, we'll fork and clone the `talawa-admin` repository. +3. Navigate to [https://github.com/PalisadoesFoundation/talawa-admin/](hhttps://github.com/PalisadoesFoundation/talawa-admin/) and click on the `fork` button. It is placed on the right corner opposite the repository name `PalisadoesFoundation/talawa-admin`. +4. You should now see `talawa-admin` under your repositories. It will be marked as forked from `PalisadoesFoundation/talawa-admin` +5. Clone the repository to your local computer (replacing the values in `{{}}`): + ``` -git clone https://github.com/PalisadoesFoundation/talawa-admin +$ git clone https://github.com/{{YOUR GITHUB USERNAME}}/talawa-admin.git ``` -This will download a local copy of `talawa-admin` in that directory. +This will setup the repository and the code files locally for you. For more detailed instructions on contributing code, and managing the versions of this repository with Git, checkout [CONTRIBUTING.md here](./CONTRIBUTING.md) -## Change Directory into the Cloned Repo +**NOTE:** `All the commands we're going to execute in the following instructions will assume you are in the root directory of the cloned talawa-admin project. If you fail to do so, the commands will not work.` -Right after cloning the repo you can change the directory of your current `terminal(shell)` to the root directory of cloned repository using this command: +## Setting up npm -``` -cd talawa-admin -``` +Best way to install and manage `node.js` is making use of node version managers. Two most popular node version managers right now are [fnm](https://github.com/Schniz/fnm) and [nvm](https://github.com/nvm-sh/nvm). We'd recommend `fnm` because it's written in `rust` and is much faster than `nvm`. Install whichever one you want and follow their guide to set up `node.js` on your system. -**NOTE:** `All the commands we're going to execute in the following instructions will assume you are in the root directory of the cloned talawa-admin project. If you fail to do so, the commands will not work.` +## Setting up Typescript -## Setting up NPM +As `talawa-admin` and `talawa-api` repositories are written using [Typescript](https://www.typescriptlang.org/), you will need to install typescript on your machine. +We recommend to install `Typescript` globally on your machine by running the following command in the terminal: -If you've followed the previous steps you should have already set up node.js on your system. [Click here](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) for the official setup guide for npm. +``` +npm install -g typescript +``` + +For more details please refer to the installation guidelines provided in the [official docs](https://www.typescriptlang.org/download). ## Installing required packages/dependencies @@ -71,8 +84,11 @@ Run the following command to install the packages and dependencies required by ` ``` npm install --legacy-peer-deps ``` + # Configuration + It's important to configure Talawa-Admin. Here's how to do it. + ## Creating .env file A file named .env is required in the root directory of talawa-admin for storing environment variables used at runtime. It is not a part of the repo and you will have to create it. For a sample of `.env` file there is a file named `.env.example` in the root directory. Create a new `.env` file by copying the contents of the `.env.example` into `.env` file. Use this command: @@ -93,7 +109,7 @@ Follow the instructions from section [Setting up REACT_APP_TALAWA_URL in .env fi ## Setting up REACT_APP_TALAWA_URL in .env file -Add the endpoint for accessing talawa-api graphql service to the variable named `REACT_APP_TALAWA_URL` in the `.env` file. +Add the endpoint for accessing talawa-api graphql service to the variable named `REACT_APP_TALAWA_URL` in the `.env` file. ``` REACT_APP_TALAWA_URL="http://API-IP-ADRESS:4000/graphql/" @@ -126,6 +142,7 @@ REACT_APP_RECAPTCHA_SITE_KEY="this_is_the_recaptcha_key" ``` # Post Configuration Steps + It's now time to start Talawa-Admin and get it running ## Running Talawa-Admin @@ -148,12 +165,12 @@ http://localhost:3000/ The first time you navigate to the running talawa-admin's website you'll land at talawa-admin registration page. Sign up using whatever credentials you want and create the account. Make sure to remember the email and password you entered because they'll be used to sign you in later on. - ## Talawa-Admin Login Now sign in to talawa-admin using the `email` and `password` you used to sign up. # Testing + It is important to test our code. If you are a contributor, please follow these steps. ## Running tests @@ -177,7 +194,6 @@ You don't need to re-run the `npm run jest-preview` command each time, simply ru ![Debugging Test Demo](./public/images/jest-preview.webp) - ## Linting code files You can lint your code files using this command: @@ -188,30 +204,24 @@ npm run lint:fix ## Husky for Git Hooks - We are using the package `Husky` to run git hooks that run according to different git workflows. -
#### pre-commit hook -We run a pre-commit hook which automatically runs code quality checks each time you make a commit and also fixes some of the issues. This way you don't have to run them manually each time. +We run a pre-commit hook which automatically runs code quality checks each time you make a commit and also fixes some of the issues. This way you don't have to run them manually each time. If you don't want these pre-commit checks running on each commit, you can manually opt out of it using the `--no-verify` flag with your commit message as shown:- git commit -m "commit message" --no-verify -
- #### post-merge hook We are also running a post-merge(post-pull) hook which will automatically run "npm install --legacy-peer-deps" only if there is any change made to pakage.json file so that the developer has all the required dependencies when pulling files from remote. - If you don't want this hook to run, you can manually opt out of this using the `no verify` flag while using the merge command(git pull): - git pull --no-verify + git pull --no-verify
- diff --git a/ISSUE_GUIDELINES.md b/ISSUE_GUIDELINES.md index 18b93ee10b..d57420d983 100644 --- a/ISSUE_GUIDELINES.md +++ b/ISSUE_GUIDELINES.md @@ -6,18 +6,18 @@ In order to give everyone a chance to submit a issues reports and contribute to ___ ## Table of Contents - - -- [Issue Report Guidelines](#issue-report-guidelines) - - [Table of Contents](#table-of-contents) - - [Issue Management](#issue-management) - - [New Issues](#new-issues) - - [Existing Issues](#existing-issues) - - [Feature Request Issues](#feature-request-issues) - - [Monitoring the Creation of New Issues](#monitoring-the-creation-of-new-issues) - - [General Guidelines](#general-guidelines) - - + + + +- [Issue Management](#issue-management) + - [New Issues](#new-issues) + - [Existing Issues](#existing-issues) + - [Feature Request Issues](#feature-request-issues) + - [Monitoring the Creation of New Issues](#monitoring-the-creation-of-new-issues) +- [General Guidelines](#general-guidelines) + + + ___ ## Issue Management diff --git a/PR_GUIDELINES.md b/PR_GUIDELINES.md index d1c2a0b2d9..4c904c782d 100644 --- a/PR_GUIDELINES.md +++ b/PR_GUIDELINES.md @@ -4,6 +4,18 @@ In order to give everyone a chance to submit a pull request and contribute to the Talawa project, we have put restrictions in place. This section outlines the guidelines that should be imposed upon pull requests in the Talawa project. +# Table of Contents + + + +- [Pull Requests and Issues](#pull-requests-and-issues) +- [Linting and Formatting](#linting-and-formatting) +- [Testing](#testing) +- [Pull Request Processing](#pull-request-processing) + - [Only submit PRs against our `develop` branch, not the default `main` branch](#only-submit-prs-against-our-develop-branch-not-the-default-main-branch) + + + ## Pull Requests and Issues 1. Do not start working on any open issue and raise a PR unless the issue is assigned to you. PRs that don't meet these guidelines will be closed. diff --git a/README.md b/README.md index 39be8dbb3d..5ee7c6d04c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,16 @@ Core features include: `talawa` is based on the original `quito` code created by the [Palisadoes Foundation][pfd] as part of its annual Calico Challenge program. Calico provides paid summer internships for Jamaican university students to work on selected open source projects. They are mentored by software professionals and receive stipends based on the completion of predefined milestones. Calico was started in 2015. Visit [The Palisadoes Foundation's website](http://www.palisadoes.org/) for more details on its origin and activities. +# Table of Contents + + + +- [Talawa Components](#talawa-components) +- [Documentation](#documentation) +- [Installation](#installation) + + + # Talawa Components `talawa` has these major software components: @@ -37,4 +47,4 @@ Core features include: # Installation -[Follow this guide](https://github.com/PalisadoesFoundation/talawa-admin/blob/develop/INSTALLATION.md) +[Follow this guide](https://github.com/PalisadoesFoundation/talawa-admin/blob/develop/INSTALLATION.md) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 12fc506498..24265fce6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "i18next-http-backend": "^1.4.1", "jest-docblock": "^27.4.0", "js-cookie": "^3.0.1", + "markdown-toc": "^1.2.0", "node-sass": "^9.0.0", "prettier": "^2.3.2", "react": "^17.0.2", @@ -5966,6 +5967,17 @@ "ansi-html": "bin/ansi-html" } }, + "node_modules/ansi-red": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", + "integrity": "sha512-ewaIr5y+9CUTGFwZfpECUbFlGcC0GCw1oqR9RI6h1gQCd9Aj2GxSckCnPsVJnmfMZbwFYE+leZGASgkWl06Jow==", + "dependencies": { + "ansi-wrap": "0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -5993,6 +6005,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha512-ZyznvL8k/FZeQHr2T6LzcJ/+vBApDnMNZvfVFy3At0knswWd6rJ3/0Hhmpu8oqa6C92npmozs890sX9Dl6q+Qw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", @@ -6317,6 +6337,14 @@ "node": ">= 4.5.0" } }, + "node_modules/autolinker": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/autolinker/-/autolinker-0.28.1.tgz", + "integrity": "sha512-zQAFO1Dlsn69eXaO6+7YZc+v84aquQKbwpzCE3L0stj56ERn9hutFxPopViLjo9G+rWwjozRhgS5KJ25Xy19cQ==", + "dependencies": { + "gulp-header": "^1.7.1" + } + }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -7712,6 +7740,19 @@ "node": ">=4" } }, + "node_modules/coffee-script": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", + "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", + "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -7859,6 +7900,68 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/concat-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/concat-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/concat-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/concat-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/concat-with-sourcemaps": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/concat-with-sourcemaps/-/concat-with-sourcemaps-1.1.0.tgz", + "integrity": "sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==", + "dependencies": { + "source-map": "^0.6.1" + } + }, + "node_modules/concat-with-sourcemaps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/configstore": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", @@ -8857,6 +8960,14 @@ "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" }, + "node_modules/diacritics-map": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/diacritics-map/-/diacritics-map-0.1.0.tgz", + "integrity": "sha512-3omnDTYrGigU0i4cJjvaKwD52B8aoqyX/NEIkukFFkogBemsIbhSa1O414fpTp5nuszJG6lvQ5vBvDVNCbSsaQ==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -10456,6 +10567,70 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha512-AFASGfIlnIbkKPQwX1yHaDjFvh/1gyKJODme52V6IORh69uEYgZp0o9C+qsIGNVEiuuhQU0CSSl++Rlegg1qvA==", + "dependencies": { + "fill-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/fill-range": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", + "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "dependencies": { + "is-number": "^2.1.0", + "isobject": "^2.0.0", + "randomatic": "^3.0.0", + "repeat-element": "^1.1.2", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/expand-range/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dependencies": { + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-range/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expand-tilde": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", @@ -10980,7 +11155,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11658,6 +11832,51 @@ "graphql": ">=0.11 <=16" } }, + "node_modules/gray-matter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-2.1.1.tgz", + "integrity": "sha512-vbmvP1Fe/fxuT2QuLVcqb2BfK7upGhhbLIt9/owWEvPYrZZEkelLcq2HqzxosV+PQ67dUFLaAeNpH7C4hhICAA==", + "dependencies": { + "ansi-red": "^0.1.1", + "coffee-script": "^1.12.4", + "extend-shallow": "^2.0.1", + "js-yaml": "^3.8.1", + "toml": "^2.3.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gray-matter/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gray-matter/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/gulp-header": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/gulp-header/-/gulp-header-1.8.12.tgz", + "integrity": "sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==", + "deprecated": "Removed event-stream from gulp-header", + "dependencies": { + "concat-with-sourcemaps": "*", + "lodash.template": "^4.4.0", + "through2": "^2.0.0" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -12619,8 +12838,7 @@ "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-callable": { "version": "1.2.7", @@ -12720,7 +12938,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, "dependencies": { "is-plain-object": "^2.0.4" }, @@ -12863,7 +13080,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -13036,7 +13252,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -16076,6 +16291,17 @@ "shell-quote": "^1.7.3" } }, + "node_modules/lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha512-7vp2Acd2+Kz4XkzxGxaB1FWOi8KjWIWsgdfD5MCb86DWvlLqhRPM+d6Pro3iNEL5VT9mstz5hKAlcd+QR6H3aA==", + "dependencies": { + "set-getter": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", @@ -16116,6 +16342,61 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/list-item": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/list-item/-/list-item-1.1.1.tgz", + "integrity": "sha512-S3D0WZ4J6hyM8o5SNKWaMYB1ALSacPZ2nHGEuCjmHZ+dc03gFeNZoNDcqfcnO4vDhTZmNrqrpYZCdXsRh22bzw==", + "dependencies": { + "expand-range": "^1.8.1", + "extend-shallow": "^2.0.1", + "is-number": "^2.1.0", + "repeat-string": "^1.5.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/list-item/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/list-item/node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/list-item/node_modules/is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha512-QUzH43Gfb9+5yckcrSA0VBDwEtDUchrk4F6tfJZQuNzDJbEDB9cZNzSfXGQ1jqmdDY/kl41lUOWM9syA8z8jlg==", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/list-item/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -16217,6 +16498,11 @@ "integrity": "sha512-RrL9VxMEPyDMHOd9uFbvMe8X55X16/cGM5IgOKgRElQZutpX89iS6vwl64duTV1/16w5JY7tuFNXqoekmh1EmA==", "dev": true }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==" + }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -16287,6 +16573,23 @@ "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -16466,6 +16769,39 @@ "node": ">=0.10.0" } }, + "node_modules/markdown-link": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/markdown-link/-/markdown-link-0.1.1.tgz", + "integrity": "sha512-TurLymbyLyo+kAUUAV9ggR9EPcDjP/ctlv9QAFiqUH7c+t6FlsbivPo9OKTU8xdOx9oNd2drW/Fi5RRElQbUqA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-toc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/markdown-toc/-/markdown-toc-1.2.0.tgz", + "integrity": "sha512-eOsq7EGd3asV0oBfmyqngeEIhrbkc7XVP63OwcJBIhH2EpG2PzFcbZdhy1jutXSlRBBVMNXHvMtSr5LAxSUvUg==", + "dependencies": { + "concat-stream": "^1.5.2", + "diacritics-map": "^0.1.0", + "gray-matter": "^2.1.0", + "lazy-cache": "^2.0.2", + "list-item": "^1.1.1", + "markdown-link": "^0.1.1", + "minimist": "^1.2.0", + "mixin-deep": "^1.1.3", + "object.pick": "^1.2.0", + "remarkable": "^1.7.1", + "repeat-string": "^1.6.1", + "strip-color": "^0.1.0" + }, + "bin": { + "markdown-toc": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -16477,6 +16813,11 @@ "node": ">= 12" } }, + "node_modules/math-random": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", + "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==" + }, "node_modules/mdn-data": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", @@ -16817,7 +17158,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, "dependencies": { "for-in": "^1.0.2", "is-extendable": "^1.0.1" @@ -17585,7 +17925,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -19678,6 +20017,27 @@ "node": ">=0.12" } }, + "node_modules/randomatic": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", + "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", + "dependencies": { + "is-number": "^4.0.0", + "kind-of": "^6.0.0", + "math-random": "^1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/randomatic/node_modules/is-number": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", + "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -20565,6 +20925,21 @@ "node": ">= 0.10" } }, + "node_modules/remarkable": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/remarkable/-/remarkable-1.7.4.tgz", + "integrity": "sha512-e6NKUXgX95whv7IgddywbeN/ItCkWbISmc2DiqHJb0wTrqZIexqdco5b8Z3XZoo/48IdNVKM9ZCvTPJ4F5uvhg==", + "dependencies": { + "argparse": "^1.0.10", + "autolinker": "~0.28.0" + }, + "bin": { + "remarkable": "bin/remarkable.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -20668,7 +21043,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -20677,7 +21051,6 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, "engines": { "node": ">=0.10" } @@ -21938,6 +22311,17 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-getter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz", + "integrity": "sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==", + "dependencies": { + "to-object-path": "^0.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -22821,6 +23205,14 @@ "node": ">=8" } }, + "node_modules/strip-color": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/strip-color/-/strip-color-0.1.0.tgz", + "integrity": "sha512-p9LsUieSjWNNAxVCXLeilaDlmuUOrDS5/dF9znM1nZc7EGX5+zEFC0bEevsNIaldjlks+2jns5Siz6F9iK6jwA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", @@ -23422,6 +23814,47 @@ "node": ">=8" } }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -23459,7 +23892,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, "dependencies": { "kind-of": "^3.0.2" }, @@ -23471,7 +23903,6 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, "dependencies": { "is-buffer": "^1.1.5" }, @@ -23522,6 +23953,11 @@ "node": ">=0.6" } }, + "node_modules/toml": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz", + "integrity": "sha512-gVweAectJU3ebq//Ferr2JUY4WKSDe5N+z0FvjDncLGyHmIDoxgY/2Ie4qfEIDm4IS7OA6Rmdm7pdEEdMcV/xQ==" + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -23743,6 +24179,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -25066,6 +25507,14 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 122567c2d7..600a547f4e 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "i18next-http-backend": "^1.4.1", "jest-docblock": "^27.4.0", "js-cookie": "^3.0.1", + "markdown-toc": "^1.2.0", "node-sass": "^9.0.0", "prettier": "^2.3.2", "react": "^17.0.2", @@ -79,7 +80,8 @@ "format:check": "prettier --check \"**/*.{ts,tsx,json,scss,css}\"", "typecheck": "tsc --project tsconfig.json --noEmit", "prepare": "husky install", - "jest-preview": "jest-preview" + "jest-preview": "jest-preview", + "update:toc": "node scripts/githooks/update-toc.js" }, "eslintConfig": { "extends": [ @@ -127,5 +129,8 @@ "@types/react": "17.0.2", "@types/react-dom": "17.0.2", "graphql": "^16.5.0" + }, + "engines": { + "node": ">=20.x" } } diff --git a/public/locales/en.json b/public/locales/en.json index de4787b7b2..7f07275c54 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -94,6 +94,8 @@ "displayImage": "Display Image", "enterName": "Enter Name", "sort": "Sort", + "Latest": "Latest", + "Earliest": "Earliest", "filter": "Filter", "cancel": "Cancel", "noOrgErrorTitle": "Organizations Not Found", @@ -147,7 +149,8 @@ "noOrgError": "Organizations not found, please create an organization through dashboard", "roleUpdated": "Role Updated.", "noResultsFoundFor": "No results found for ", - "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too." + "talawaApiUnavailable": "Talawa-API service is unavailable. Is it running? Check your network connectivity too.", + "cancel": "Cancel" }, "requests": { "title": "Talawa Requests", @@ -599,7 +602,12 @@ "home": { "feed": "Feed", "pinnedPosts": "View Pinned Posts", - "somethingOnYourMind": "Something on your mind?" + "somethingOnYourMind": "Something on your mind?", + "addPost": "Add Post", + "startPost": "Start a post", + "media": "Media", + "event": "Event", + "article": "Article" }, "settings": { "profileSettings": "Profile Settings", @@ -607,7 +615,10 @@ "lastName": "Last Name", "emailAddress": "Email Address", "updateImage": "Update Image", - "save": "Save" + "saveChanges": "Save Changes", + "updateProfile": "Update Profile", + "otherSettings": "Other Settings", + "changeLanguage": "Change Language" }, "donate": { "donateTo": "Donate to", @@ -682,11 +693,7 @@ "deleteAdvertisement": "Delete Advertisement", "deleteAdvertisementMsg": "Do you want to remove this advertisement?", "no": "No", - "yes": "Yes", - "view": "View", - "edit": "Edit", - "editAdvertisement": "Edit Advertisement", - "saveChanges": "Save Changes" + "yes": "Yes" }, "userChat": { "chat": "Chat", diff --git a/public/locales/fr.json b/public/locales/fr.json index dacb926a6f..b626994c5c 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -33,6 +33,11 @@ "eventCardSeeAll": "Voir Tout", "noEvents": "Aucun événement à venir" }, + "latestPosts": { + "latestPostsTitle": "Dernières Publications", + "seeAllLink": "Voir Tout", + "noPostsCreated": "Aucune Publication Créée" + }, "listNavbar": { "talawa_portal": "Portail D'Administrateur Talawa", "roles": "Les rôles", @@ -88,6 +93,8 @@ "displayImage": "Afficher l'image", "enterName": "Entrez le nom", "sort": "Trier", + "Earliest": "Le plus tôt", + "Latest": "Dernière", "filter": "Filtre", "cancel": "Annuler", "endOfResults": "Fin des résultats", @@ -137,7 +144,8 @@ "filter": "Filtre", "roleUpdated": "Rôle mis à jour.", "noResultsFoundFor": "Aucun résultat trouvé pour ", - "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau." + "talawaApiUnavailable": "Le service Talawa-API n'est pas disponible. Est-il en cours d'exécution ? Vérifiez également votre connectivité réseau.", + "cancel": "Annuler" }, "requests": { "title": "Demandes Talawa", @@ -589,7 +597,12 @@ "home": { "feed": "Alimentation", "pinnedPosts": "Afficher les publications épinglées", - "somethingOnYourMind": "Quelque chose dans votre esprit?" + "somethingOnYourMind": "Quelque chose dans votre esprit?", + "addPost": "Ajouter une publication", + "startPost": "Commencer une publication", + "media": "Médias", + "event": "Événement", + "article": "Article" }, "settings": { "profileSettings": "Paramètres de profil", @@ -597,7 +610,10 @@ "lastName": "Nom de famille", "emailAddress": "Adresse e-mail", "updateImage": "Mettre à jour l'image", - "save": "Sauvegarder" + "saveChanges": "Sauvegarder les modifications", + "updateProfile": "Mettre à jour le profil", + "otherSettings": "Autres paramètres", + "changeLanguage": "Changer la langue" }, "donate": { "donateTo": "Faire un don à", @@ -662,11 +678,7 @@ "deleteAdvertisement": "Supprimer l'annonce", "deleteAdvertisementMsg": "Voulez-vous supprimer cette annonce ?", "no": "Non", - "yes": "Oui", - "view": "Voir", - "edit": "Éditer", - "editAdvertisement": "Éditer l'annonce", - "saveChanges": "Enregistrer les modifications" + "yes": "Oui" }, "userChat": { "chat": "Chat", diff --git a/public/locales/hi.json b/public/locales/hi.json index f470e327a8..5f272bad8f 100644 --- a/public/locales/hi.json +++ b/public/locales/hi.json @@ -33,6 +33,11 @@ "eventCardSeeAll": "सभी देखें", "noEvents": "कोई आगामी घटनाएँ नहीं" }, + "latestPosts": { + "latestPostsTitle": "नवीनतम पोस्ट", + "seeAllLink": "सभी देखें", + "noPostsCreated": "कोई पोस्ट नहीं बनाई गई" + }, "listNavbar": { "talawa_portal": "तलावा प्रशासन पोर्टल", "roles": "भूमिकाएँ", @@ -88,6 +93,8 @@ "displayImage": "प्रदर्शन छवि", "enterName": "नाम दर्ज करें", "sort": "छांटें", + "Earliest": "सबसे पुराना", + "Latest": "सबसे नवीनतम", "filter": "फ़िल्टर", "cancel": "रद्द करना", "endOfResults": "परिणामों का अंत", @@ -136,7 +143,8 @@ "filter": "फ़िल्टर", "roleUpdated": "भूमिका अपडेट की गई।", "noResultsFoundFor": "के लिए कोई परिणाम नहीं मिला ", - "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।" + "talawaApiUnavailable": "तलवा-एपीआई सेवा उपलब्ध नहीं है। क्या यह चल रहा है? अपनी नेटवर्क कनेक्टिविटी भी जांचें।", + "cancel": "रद्द करें" }, "requests": { "title": "तलवा अनुरोध", @@ -589,7 +597,12 @@ "home": { "feed": "फ़ीड", "pinnedPosts": "पिन किए गए पोस्ट देखें", - "somethingOnYourMind": "आपके मन में कुछ है?" + "somethingOnYourMind": "आपके मन में कुछ है?", + "addPost": "पोस्ट जोड़ें", + "startPost": "एक पोस्ट शुरू करें", + "media": "मीडिया", + "event": "घटना", + "article": "लेख" }, "settings": { "profileSettings": "पार्श्वचित्र समायोजन", @@ -597,7 +610,10 @@ "lastName": "उपनाम", "emailAddress": "मेल पता", "updateImage": "छवि अद्यतन करें", - "save": "बचाना" + "saveChanges": "परिवर्तनों को सुरक्षित करें", + "updateProfile": "प्रोफ़ाइल अपडेट करें", + "otherSettings": "अन्य सेटिंग्स", + "changeLanguage": "भाषा बदलें" }, "donate": { "donateTo": "दान दें", @@ -662,11 +678,7 @@ "deleteAdvertisement": "विज्ञापन हटाएं", "deleteAdvertisementMsg": "क्या आप इस विज्ञापन को हटाना चाहते हैं?", "no": "नहीं", - "yes": "हाँ", - "view": "देखें", - "edit": "संपादित करें", - "editAdvertisement": "विज्ञापन संपादित करें", - "saveChanges": "परिवर्तन सहेजें" + "yes": "हाँ" }, "userChat": { "chat": "बात", @@ -677,14 +689,14 @@ "selectContact": "बातचीत शुरू करने के लिए एक संपर्क चुनें", "sendMessage": "मेसेज भेजें" }, - "ऑर्गप्रोफ़ाइलफ़ील्ड": { - "लोड हो रहा है": "लोड हो रहा है...", - "noCustomField": "कोई कस्टम फ़ील्ड उपलब्ध नहीं", - "customFieldName": "फ़ील्ड नाम", - "enterCustomFieldName": "फ़ील्ड नाम दर्ज करें", - "customFieldType": "फ़ील्ड प्रकार", + "orgProfileField": { + "loading": "लोड हो रहा है...", + "noCustomField": "कोई कस्टम फ़ील्ड उपलब्ध नहीं है", + "customFieldName": "फ़ील्ड का नाम", + "enterCustomFieldName": "फ़ील्ड का नाम दर्ज करें", + "customFieldType": "फ़ील्ड का प्रकार", "saveChanges": "परिवर्तन सहेजें", - "कस्टम फ़ील्ड हटाएँ": "कस्टम फ़ील्ड हटाएँ", + "Remove Custom Field": "कस्टम फ़ील्ड हटाएँ", "fieldSuccessMessage": "फ़ील्ड सफलतापूर्वक जोड़ा गया", "fieldRemovalSuccess": "फ़ील्ड सफलतापूर्वक हटा दिया गया" } diff --git a/public/locales/sp.json b/public/locales/sp.json index c273777949..abf86c60dc 100644 --- a/public/locales/sp.json +++ b/public/locales/sp.json @@ -33,6 +33,11 @@ "eventCardSeeAll": "Ver Todos", "noEvents": "No Hay Eventos Próximos" }, + "latestPosts": { + "latestPostsTitle": "Últimas Publicaciones", + "seeAllLink": "Ver Todo", + "noPostsCreated": "No se han creado publicaciones" + }, "listNavbar": { "talawa_portal": "Portal De Administración Talawa", "roles": "Roles", @@ -88,6 +93,8 @@ "displayImage": "Mostrar imagen", "enterName": "Ingrese su nombre", "sort": "Ordenar", + "Earliest": "Más Temprano", + "Latest": "El último", "filter": "Filtrar", "cancel": "Cancelar", "endOfResults": "Fin de los resultados", @@ -136,7 +143,8 @@ "filter": "Filtrar", "roleUpdated": "Rol actualizado.", "noResultsFoundFor": "No se encontraron resultados para ", - "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red." + "talawaApiUnavailable": "El servicio Talawa-API no está disponible. ¿Está funcionando? Compruebe también la conectividad de su red.", + "cancel": "Cancelar" }, "requests": { "title": "Solicitudes Talawa", @@ -589,7 +597,12 @@ "home": { "feed": "Alimentar", "pinnedPosts": "Ver publicaciones fijadas", - "somethingOnYourMind": "¿Algo en tu mente?" + "somethingOnYourMind": "¿Algo en tu mente?", + "addPost": "Agregar publicación", + "startPost": "Comenzar una publicación", + "media": "Medios", + "event": "Evento", + "article": "Artículo" }, "settings": { "profileSettings": "Configuración de perfil", @@ -597,7 +610,10 @@ "lastName": "Apellido", "emailAddress": "dirección de correo electrónico", "updateImage": "Actualizar imagen", - "save": "Ahorrar" + "saveChanges": "Guardar cambios", + "updateProfile": "Actualización del perfil", + "otherSettings": "Otras Configuraciones", + "changeLanguage": "Cambiar Idioma" }, "donate": { "donateTo": "Donar a", @@ -662,11 +678,7 @@ "deleteAdvertisement": "Eliminar anuncio", "deleteAdvertisementMsg": "¿Desea eliminar este anuncio?", "no": "No", - "yes": "Sí", - "view": "Ver", - "edit": "Editar", - "editAdvertisement": "Editar Anuncio", - "saveChanges": "Guardar Cambios" + "yes": "Sí" }, "userChat": { "chat": "Charlar", @@ -677,15 +689,16 @@ "selectContact": "Seleccione un contacto para iniciar una conversación", "sendMessage": "Enviar mensaje" }, - "campoPerfildeOrganización": { - "cargando": "Cargando...", + + "orgProfileField": { + "loading": "Cargando..", "noCustomField": "No hay campos personalizados disponibles", - "customFieldName": "Nombre de campo", - "enterCustomFieldName": "Ingrese el nombre del campo", - "customFieldType": "Tipo de campo", - "saveChanges": "Guardar cambios", - "Eliminar campo personalizado": "Eliminar campo personalizado", - "fieldSuccessMessage": "Campo agregado exitosamente", + "customFieldName": "Nombre del Campo", + "enterCustomFieldName": "Ingrese el Nombre del Campo", + "customFieldType": "Tipo de Campo", + "saveChanges": "Guardar Cambios", + "Remove Custom Field": "Eliminar Campo Personalizado", + "fieldSuccessMessage": "Campo añadido exitosamente", "fieldRemovalSuccess": "Campo eliminado exitosamente" } } diff --git a/public/locales/zh.json b/public/locales/zh.json index b03c3ac8e2..30a048d1be 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -33,6 +33,11 @@ "eventCardSeeAll": "查看全部", "noEvents": "暂无即将举行的活动" }, + "latestPosts": { + "latestPostsTitle": "最新文章", + "seeAllLink": "查看全部", + "noPostsCreated": "暂无文章" + }, "listNavbar": { "talawa_portal": "塔拉瓦管理門戶", "roles": "角色", @@ -88,6 +93,8 @@ "displayImage": "顯示圖像", "enterName": "输入名字", "sort": "排序", + "Earliest": "最早的", + "Latest": "最新的", "filter": "過濾", "cancel": "取消", "endOfResults": "結果結束", @@ -136,7 +143,8 @@ "filter": "過濾", "roleUpdated": "角色已更新。", "noResultsFoundFor": "未找到结果 ", - "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。" + "talawaApiUnavailable": "服務不可用。它在運行嗎?還要檢查您的網絡連接。", + "cancel": "取消" }, "requests": { "title": "塔拉瓦請求", @@ -589,7 +597,12 @@ "home": { "feed": "餵養", "pinnedPosts": "查看固定帖子", - "somethingOnYourMind": "你有什麼心事嗎?" + "somethingOnYourMind": "你有什麼心事嗎?", + "addPost": "添加帖子", + "startPost": "开始一篇帖子", + "media": "媒体", + "event": "活动", + "article": "文章" }, "settings": { "profileSettings": "配置文件設置", @@ -597,7 +610,10 @@ "lastName": "姓", "emailAddress": "電子郵件地址", "updateImage": "更新圖片", - "save": "節省" + "saveChanges": "保存更改", + "updateProfile": "更新个人信息", + "otherSettings": "其他设置", + "changeLanguage": "更改语言" }, "donate": { "donateTo": "捐贈給", @@ -662,11 +678,7 @@ "deleteAdvertisement": "删除广告", "deleteAdvertisementMsg": "您是否要删除此广告?", "no": "不", - "yes": "是", - "view": "查看", - "edit": "编辑", - "editAdvertisement": "编辑广告", - "saveChanges": "保存更改" + "yes": "是" }, "userChat": { "chat": "聊天", diff --git a/schema.graphql b/schema.graphql index d200ee7349..0d8c984c9f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -585,6 +585,8 @@ input OrganizationInput { enum OrganizationOrderByInput { apiUrl_ASC apiUrl_DESC + createdAt_ASC + createdAt_DESC description_ASC description_DESC id_ASC diff --git a/scripts/githooks/update-toc.js b/scripts/githooks/update-toc.js new file mode 100644 index 0000000000..268becfd13 --- /dev/null +++ b/scripts/githooks/update-toc.js @@ -0,0 +1,14 @@ +import fs from 'fs'; +import { execSync } from 'child_process'; + +const markdownFiles = fs + .readdirSync('./') + .filter((file) => file.endsWith('.md')); + +markdownFiles.forEach((file) => { + const command = `markdown-toc -i "${file}" --bullets "-"`; + execSync(command, { stdio: 'inherit' }); + +}); + +console.log('Table of contents updated successfully.'); diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts index a1a755eeaf..f25abab4ef 100644 --- a/src/GraphQl/Mutations/mutations.ts +++ b/src/GraphQl/Mutations/mutations.ts @@ -440,29 +440,6 @@ export const ADD_ADVERTISEMENT_MUTATION = gql` } } `; -export const UPDATE_ADVERTISEMENT_MUTATION = gql` - mutation UpdateAdvertisement( - $id: ID! - $name: String - $link: String - $type: String - $startDate: Date - $endDate: Date - ) { - updateAdvertisement( - id: $id - data: { - name: $name - link: $link - type: $type - startDate: $startDate - endDate: $endDate - } - ) { - _id - } - } -`; export const DELETE_ADVERTISEMENT_BY_ID = gql` mutation ($id: ID!) { deleteAdvertisementById(id: $id) { diff --git a/src/GraphQl/Queries/Queries.ts b/src/GraphQl/Queries/Queries.ts index 76db5206c2..95b149b187 100644 --- a/src/GraphQl/Queries/Queries.ts +++ b/src/GraphQl/Queries/Queries.ts @@ -40,13 +40,19 @@ export const ORGANIZATION_LIST = gql` } `; -// Query to take the Organization list with filter option +// Query to take the Organization list with filter and sort option export const ORGANIZATION_CONNECTION_LIST = gql` - query OrganizationsConnection($filter: String, $first: Int, $skip: Int) { + query OrganizationsConnection( + $filter: String + $first: Int + $skip: Int + $orderBy: OrganizationOrderByInput + ) { organizationsConnection( where: { name_contains: $filter } first: $first skip: $skip + orderBy: $orderBy ) { _id image diff --git a/src/components/Advertisements/Advertisements.tsx b/src/components/Advertisements/Advertisements.tsx index 606a65c7e1..461c7d8b9d 100644 --- a/src/components/Advertisements/Advertisements.tsx +++ b/src/components/Advertisements/Advertisements.tsx @@ -141,7 +141,6 @@ export default function advertisements(): JSX.Element { orgId={ad.orgId} startDate={new Date(ad.startDate)} endDate={new Date(ad.endDate)} - link={ad.link} // getInstalledPlugins={getInstalledPlugins} /> ) diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css index 20bb86a21a..1f1ea89996 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.module.css @@ -4,12 +4,13 @@ } .entryaction { + margin-left: auto; display: flex !important; + align-items: center; } .entryaction i { margin-right: 8px; - margin-top: 4px; } .entryaction .spinner-grow { @@ -17,54 +18,3 @@ width: 1rem; margin-right: 8px; } - -.buttons { - display: flex; - justify-content: flex-end; -} - -.dropdownButton { - background-color: transparent; - color: #000; - border: none; - cursor: pointer; - display: flex; - width: 100%; - justify-content: flex-end; - padding: 8px 10px; -} - -.dropdownContainer { - position: relative; - display: inline-block; -} - -.dropdownmenu { - display: none; - position: absolute; - z-index: 1; - background-color: white; - width: 120px; - box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); - padding: 5px 0; - margin: 0; - list-style-type: none; - right: 0; - top: 100%; -} - -.dropdownmenu li { - cursor: pointer; - padding: 8px 16px; - text-decoration: none; - display: block; - color: #333; -} - -.dropdownmenu li:hover { - background-color: #f1f1f1; -} - -.dropdownContainer:hover .dropdownmenu { - display: block; -} diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.test.tsx b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.test.tsx index 429298ed6a..bedd1c1ce9 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.test.tsx +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { DELETE_ADVERTISEMENT_BY_ID } from 'GraphQl/Mutations/mutations'; -import { MockedProvider } from '@apollo/client/testing'; import { render, fireEvent, waitFor, screen } from '@testing-library/react'; + import { ApolloClient, ApolloProvider, @@ -9,7 +8,7 @@ import { ApolloLink, HttpLink, } from '@apollo/client'; -import { StaticMockLink } from 'utils/StaticMockLink'; + import type { NormalizedCacheObject } from '@apollo/client'; import { BrowserRouter } from 'react-router-dom'; import AdvertisementEntry from './AdvertisementEntry'; @@ -18,71 +17,7 @@ import { store } from 'state/store'; import { BACKEND_URL } from 'Constant/constant'; import i18nForTest from 'utils/i18nForTest'; import { I18nextProvider } from 'react-i18next'; -import { act } from 'react-dom/test-utils'; -import { ADVERTISEMENTS_GET } from 'GraphQl/Queries/Queries'; - -const advertisementProps = { - id: '1', - name: 'Sample Advertisement', - type: 'Sample Type', - orgId: 'org_id', - link: 'samplelink.com', - endDate: new Date(), - startDate: new Date(), -}; -const mocks = [ - { - request: { - query: DELETE_ADVERTISEMENT_BY_ID, - variables: { id: '1' }, - }, - result: { - data: { - deleteAdvertisementById: { - success: true, - }, - }, - }, - }, - { - request: { - query: ADVERTISEMENTS_GET, - }, - result: { - data: { - getAdvertisements: [ - { - _id: '6574cf9caa18987e28d248d9', - name: 'Cookie', - orgId: '6437904485008f171cf29924', - link: '123', - type: 'BANNER', - startDate: '2023-12-10', - endDate: '2023-12-10', - }, - { - _id: '6574e38aaa18987e28d24979', - name: 'HEy', - orgId: '6437904485008f171cf29924', - link: '123', - type: 'BANNER', - startDate: '2023-12-10', - endDate: '2023-12-10', - }, - ], - }, - }, - }, -]; -const link = new StaticMockLink(mocks, true); -async function wait(ms = 100): Promise { - await act(() => { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); - }); -} const httpLink = new HttpLink({ uri: BACKEND_URL, headers: { @@ -178,69 +113,4 @@ describe('Testing Advertisement Entry Component', () => { expect(deletionFailedText).toBeNull(); }); }); - it('should open and close the dropdown when options button is clicked', () => { - const { getByTestId, queryByText, getAllByText } = render( - - - - - - - - - - ); - - // Test initial rendering - expect(getByTestId('AdEntry')).toBeInTheDocument(); - expect(getAllByText('POPUP')[0]).toBeInTheDocument(); - expect(getAllByText('Advert1')[0]).toBeInTheDocument(); - - // Test dropdown functionality - const optionsButton = getByTestId('moreiconbtn'); - - // Initially, the dropdown should not be visible - expect(queryByText('Edit')).toBeNull(); - - // Click to open the dropdown - fireEvent.click(optionsButton); - - // After clicking the button, the dropdown should be visible - expect(queryByText('Edit')).toBeInTheDocument(); - - // Click again to close the dropdown - fireEvent.click(optionsButton); - - // After the second click, the dropdown should be hidden again - expect(queryByText('Edit')).toBeNull(); - }); - test('should delete an advertisement when delete button is clicked', async () => { - const { getByTestId } = render( - - - - - - - - - - - - ); - await wait(); - const optionsButton = getByTestId('moreiconbtn'); - fireEvent.click(optionsButton); - const deleteButton = getByTestId('deletebtn'); - fireEvent.click(deleteButton); - }); }); diff --git a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx index ca665e8ca1..3f932ad848 100644 --- a/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx +++ b/src/components/Advertisements/core/AdvertisementEntry/AdvertisementEntry.tsx @@ -6,8 +6,6 @@ import { DELETE_ADVERTISEMENT_BY_ID } from 'GraphQl/Mutations/mutations'; import { useMutation } from '@apollo/client'; import { useTranslation } from 'react-i18next'; import { ADVERTISEMENTS_GET } from 'GraphQl/Queries/Queries'; -import AdvertisementRegister from '../AdvertisementRegister/AdvertisementRegister'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; import { toast } from 'react-toastify'; interface InterfaceAddOnEntryProps { id: string; @@ -31,8 +29,8 @@ function advertisementEntry({ }: InterfaceAddOnEntryProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); const [buttonLoading, setButtonLoading] = useState(false); - const [dropdown, setDropdown] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); + const [deleteAdById] = useMutation(DELETE_ADVERTISEMENT_BY_ID, { refetchQueries: [ADVERTISEMENTS_GET], }); @@ -53,43 +51,12 @@ function advertisementEntry({ setButtonLoading(false); } }; - const handleOptionsClick = (): void => { - setDropdown(!dropdown); - }; return ( <> {Array.from({ length: 1 }).map((_, idx) => ( -
- - {dropdown && ( -
    -
  • - -
  • -
  • - {t('delete')} -
  • -
- )} -
{type} - {link} -
- -
{link} +
diff --git a/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.module.css b/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.module.css index 646311041a..c122d386fa 100644 --- a/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.module.css +++ b/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.module.css @@ -4,16 +4,6 @@ align-items: center; } -.modalbtn i, -.button i { +.modalbtn i { margin-right: 8px; } - -.button { - min-width: 102px; -} - -.editHeader { - background-color: #31bb6b; - color: white; -} diff --git a/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx b/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx index 3686c20f31..ff9391fa7b 100644 --- a/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx +++ b/src/components/Advertisements/core/AdvertisementRegister/AdvertisementRegister.tsx @@ -1,12 +1,9 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import styles from './AdvertisementRegister.module.css'; import { Button, Form, Modal } from 'react-bootstrap'; -import { - ADD_ADVERTISEMENT_MUTATION, - UPDATE_ADVERTISEMENT_MUTATION, -} from 'GraphQl/Mutations/mutations'; import { useMutation, useQuery } from '@apollo/client'; +import { ADD_ADVERTISEMENT_MUTATION } from 'GraphQl/Mutations/mutations'; import { useTranslation } from 'react-i18next'; import { toast } from 'react-toastify'; import dayjs from 'dayjs'; @@ -15,14 +12,6 @@ import { ADVERTISEMENTS_GET } from 'GraphQl/Queries/Queries'; interface InterfaceAddOnRegisterProps { id?: string; // OrgId createdBy?: string; // User - formStatus?: string; - idEdit?: string; - nameEdit?: string; - typeEdit?: string; - orgIdEdit?: string; - linkEdit?: string; - endDateEdit?: Date; - startDateEdit?: Date; } interface InterfaceFormStateTypes { name: string; @@ -36,16 +25,6 @@ interface InterfaceFormStateTypes { function advertisementRegister({ /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ createdBy, - formStatus, - idEdit, - nameEdit, - typeEdit, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - orgIdEdit, - linkEdit, - endDateEdit, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - startDateEdit, }: InterfaceAddOnRegisterProps): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'advertisement' }); @@ -54,7 +33,6 @@ function advertisementRegister({ const handleClose = (): void => setShow(false); const handleShow = (): void => setShow(true); const [create] = useMutation(ADD_ADVERTISEMENT_MUTATION); - const [updateAdvertisement] = useMutation(UPDATE_ADVERTISEMENT_MUTATION); const { refetch } = useQuery(ADVERTISEMENTS_GET); //getting orgId from URL @@ -67,30 +45,6 @@ function advertisementRegister({ endDate: new Date(), orgId: currentOrg, }); - - //if set to edit set the formState by edit variables - useEffect(() => { - if (formStatus === 'edit') { - setFormState((prevState) => ({ - ...prevState, - name: nameEdit || '', - link: linkEdit || '', - type: typeEdit || 'BANNER', - startDate: startDateEdit || new Date(), - endDate: endDateEdit || new Date(), - orgId: currentOrg, - })); - } - }, [ - formStatus, - nameEdit, - linkEdit, - typeEdit, - startDateEdit, - endDateEdit, - currentOrg, - ]); - const handleRegister = async (): Promise => { try { console.log('At handle register', formState); @@ -123,50 +77,20 @@ function advertisementRegister({ console.log('error occured', error); } }; - const handleUpdate = async (): Promise => { - try { - console.log('At handle update', formState); - const { data } = await updateAdvertisement({ - variables: { - id: idEdit, - // orgId: currentOrg, - name: formState.name, - link: formState.link, - type: formState.type, - startDate: dayjs(formState.startDate).format('YYYY-MM-DD'), - endDate: dayjs(formState.endDate).format('YYYY-MM-DD'), - }, - }); - - if (data) { - toast.success('Advertisement updated successfully'); - } - } catch (error: any) { - toast.error(error.message); - } - }; return ( <> - {formStatus === 'register' ? ( //If register show register button else show edit button - - ) : ( -
{t('edit')}
- )} + - - {formStatus === 'register' ? ( - {t('RClose')} - ) : ( - {t('editAdvertisement')} - )} + + {t('RClose')}
@@ -225,7 +149,7 @@ function advertisementRegister({ { setFormState({ ...formState, @@ -240,7 +164,7 @@ function advertisementRegister({ { setFormState({ ...formState, @@ -259,23 +183,13 @@ function advertisementRegister({ > {t('close')} - {formStatus === 'register' ? ( - - ) : ( - - )} + @@ -289,7 +203,6 @@ advertisementRegister.defaultProps = { startDate: new Date(), endDate: new Date(), orgId: '', - formStatus: 'register', }; advertisementRegister.propTypes = { @@ -299,7 +212,6 @@ advertisementRegister.propTypes = { startDate: PropTypes.instanceOf(Date), endDate: PropTypes.instanceOf(Date), orgId: PropTypes.string, - formStatus: PropTypes.string, }; export default advertisementRegister; diff --git a/src/components/EventStats/EventStats.module.css b/src/components/EventStats/EventStats.module.css new file mode 100644 index 0000000000..44ba75a0a8 --- /dev/null +++ b/src/components/EventStats/EventStats.module.css @@ -0,0 +1,35 @@ +.stackEvents { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 10px 20px; + padding: 10px 20px; + overflow: hidden; + gap: 2px; + column-gap: 2px; +} + +@media screen and (min-width: 801px) { + .stackEvents { + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + padding: 0 2rem; + margin: 0 40px; + gap: 5px; + column-gap: 4px; + } +} + +@media screen and (min-width: 768px) and (max-width: 800px) { + .stackEvents { + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + padding: 0 2rem; + margin: 0 20px; + gap: 5px; + column-gap: 4px; + } +} diff --git a/src/components/EventStats/EventStats.tsx b/src/components/EventStats/EventStats.tsx index 6158c39075..c9f1a70e8d 100644 --- a/src/components/EventStats/EventStats.tsx +++ b/src/components/EventStats/EventStats.tsx @@ -3,8 +3,8 @@ import { Modal } from 'react-bootstrap'; import { FeedbackStats } from './Statistics/Feedback'; import { ReviewStats } from './Statistics/Review'; import { AverageRating } from './Statistics/AverageRating'; -import Stack from '@mui/material/Stack'; import styles from './Loader.module.css'; +import eventStatsStyles from './EventStats.module.css'; import { useQuery } from '@apollo/client'; import { EVENT_FEEDBACKS } from 'GraphQl/Queries/Queries'; @@ -44,14 +44,12 @@ export const EventStats = ({ Event Statistics - - - -
- - -
-
+ + +
+ + +
diff --git a/src/components/OrgListCard/OrgListCard.module.css b/src/components/OrgListCard/OrgListCard.module.css index 935bada4f4..a33691f0c6 100644 --- a/src/components/OrgListCard/OrgListCard.module.css +++ b/src/components/OrgListCard/OrgListCard.module.css @@ -89,7 +89,7 @@ .manageBtn { display: flex; justify-content: space-around; - width: 150px; + width: 100px; } .orgName { diff --git a/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx b/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx index 57d478b8d0..841a858839 100644 --- a/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx +++ b/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx @@ -174,4 +174,65 @@ describe('Testing UserNavbar Component [User Portal]', () => { expect(cookies.get('i18next')).toBe('zh'); }); + + test('User can see and interact with the dropdown menu', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('logoutDropdown')); + expect(screen.getByText('Settings')).toBeInTheDocument(); + expect(screen.getByText('My Tasks')).toBeInTheDocument(); + expect(screen.getByTestId('logoutBtn')).toBeInTheDocument(); + }); + + test('User can navigate to the "Settings" page', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('logoutDropdown')); + userEvent.click(screen.getByText('Settings')); + expect(window.location.pathname).toBe('/user/settings'); + }); + + test('User can navigate to the "My Tasks" page', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('logoutDropdown')); + userEvent.click(screen.getByText('My Tasks')); + expect(window.location.pathname).toBe('/user/tasks'); + }); }); diff --git a/src/components/UserPortal/UserNavbar/UserNavbar.tsx b/src/components/UserPortal/UserNavbar/UserNavbar.tsx index e10268e85e..04f4bd50fb 100644 --- a/src/components/UserPortal/UserNavbar/UserNavbar.tsx +++ b/src/components/UserPortal/UserNavbar/UserNavbar.tsx @@ -8,11 +8,13 @@ import cookies from 'js-cookie'; import PermIdentityIcon from '@mui/icons-material/PermIdentity'; import LanguageIcon from '@mui/icons-material/Language'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; import { useMutation } from '@apollo/client'; import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations'; +import { useHistory } from 'react-router-dom'; function userNavbar(): JSX.Element { + const history = useHistory(); + const { t } = useTranslation('translation', { keyPrefix: 'userNavbar', }); @@ -95,16 +97,21 @@ function userNavbar(): JSX.Element { {userName} - - - {t('settings')} - + + history.push('/user/settings')} + className={styles.link} + > + {t('settings')} - - - {t('myTasks')} - + + history.push('/user/tasks')} + className={styles.link} + > + {t('myTasks')} + {t('logout')} diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx index b81b9c809c..98bb63a847 100644 --- a/src/screens/LoginPage/LoginPage.tsx +++ b/src/screens/LoginPage/LoginPage.tsx @@ -7,7 +7,7 @@ import Col from 'react-bootstrap/Col'; import Row from 'react-bootstrap/Row'; import ReCAPTCHA from 'react-google-recaptcha'; import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; +import { Link, useHistory } from 'react-router-dom'; import { toast } from 'react-toastify'; import { REACT_APP_USE_RECAPTCHA, RECAPTCHA_SITE_KEY } from 'Constant/constant'; @@ -26,6 +26,7 @@ import EmailOutlinedIcon from '@mui/icons-material/EmailOutlined'; function loginPage(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'loginPage' }); + const history = useHistory(); document.title = t('title'); @@ -51,7 +52,7 @@ function loginPage(): JSX.Element { useEffect(() => { const isLoggedIn = localStorage.getItem('IsLoggedIn'); if (isLoggedIn == 'TRUE') { - window.location.assign('/orglist'); + history.push('/orglist'); } setComponentLoader(false); }, []); @@ -196,7 +197,7 @@ function loginPage(): JSX.Element { localStorage.setItem('IsLoggedIn', 'TRUE'); localStorage.setItem('UserType', loginData.login.user.userType); if (localStorage.getItem('IsLoggedIn') == 'TRUE') { - window.location.replace('/orglist'); + history.push('/orglist'); } } else { toast.warn(t('notAuthorised')); diff --git a/src/screens/MemberDetail/MemberDetail.test.tsx b/src/screens/MemberDetail/MemberDetail.test.tsx index abd1a288b5..2647adf0fb 100644 --- a/src/screens/MemberDetail/MemberDetail.test.tsx +++ b/src/screens/MemberDetail/MemberDetail.test.tsx @@ -167,7 +167,7 @@ describe('MemberDetail', () => { expect(screen.getAllByText(/Admin for events/i)).toBeTruthy(); expect(screen.getAllByText(/Created On/i)).toHaveLength(2); - expect(screen.getAllByText(/User Details/i)).toHaveLength(3); + expect(screen.getAllByText(/User Details/i)).toHaveLength(2); expect(screen.getAllByText(/Role/i)).toHaveLength(2); expect(screen.getAllByText(/Created/i)).toHaveLength(4); expect(screen.getAllByText(/Joined/i)).toHaveLength(2); diff --git a/src/screens/MemberDetail/MemberDetail.tsx b/src/screens/MemberDetail/MemberDetail.tsx index d190b47246..0bad4fadc7 100644 --- a/src/screens/MemberDetail/MemberDetail.tsx +++ b/src/screens/MemberDetail/MemberDetail.tsx @@ -83,27 +83,16 @@ const MemberDetail: React.FC = ({ id }): JSX.Element => { <> - -
-
-
- -
-
- {state == 1 ? (
-

{t('title')}

+

+ {t('title')} +

@@ -624,7 +661,7 @@ function orgList(): JSX.Element {
{t('goToStore')} @@ -635,7 +672,7 @@ function orgList(): JSX.Element { className={styles.greenregbtn} onClick={closeDialogModal} value="invite" - data-testid="submitOrganizationForm" + data-testid="enableEverythingForm" > {t('enableEverything')} diff --git a/src/screens/OrgList/OrgListMocks.ts b/src/screens/OrgList/OrgListMocks.ts index 1a1ab30ab7..833535f4aa 100644 --- a/src/screens/OrgList/OrgListMocks.ts +++ b/src/screens/OrgList/OrgListMocks.ts @@ -1,3 +1,7 @@ +import { + CREATE_ORGANIZATION_MUTATION, + CREATE_SAMPLE_ORGANIZATION_MUTATION, +} from 'GraphQl/Mutations/mutations'; import { ORGANIZATION_CONNECTION_LIST, USER_ORGANIZATION_LIST, @@ -84,6 +88,7 @@ const MOCKS = [ first: 8, skip: 0, filter: '', + orderBy: 'createdAt_ASC', }, notifyOnNetworkStatusChange: true, }, @@ -102,6 +107,39 @@ const MOCKS = [ data: superAdminUser, }, }, + { + request: { + query: CREATE_SAMPLE_ORGANIZATION_MUTATION, + }, + result: { + data: { + createSampleOrganization: { + id: '1', + name: 'Sample Organization', + }, + }, + }, + }, + { + request: { + query: CREATE_ORGANIZATION_MUTATION, + variables: { + description: 'This is a dummy organization', + location: 'Delhi, India', + name: 'Dummy Organization', + visibleInSearch: true, + isPublic: false, + image: '', + }, + }, + result: { + data: { + createOrganization: { + _id: '1', + }, + }, + }, + }, ]; const MOCKS_EMPTY = [ { @@ -111,6 +149,7 @@ const MOCKS_EMPTY = [ first: 8, skip: 0, filter: '', + orderBy: 'createdAt_ASC', }, notifyOnNetworkStatusChange: true, }, @@ -130,6 +169,40 @@ const MOCKS_EMPTY = [ }, }, ]; +const MOCKS_WITH_ERROR = [ + { + request: { + query: ORGANIZATION_CONNECTION_LIST, + variables: { + first: 8, + skip: 0, + filter: '', + orderBy: 'createdAt_ASC', + }, + notifyOnNetworkStatusChange: true, + }, + result: { + data: { + organizationsConnection: organizations, + }, + }, + }, + { + request: { + query: USER_ORGANIZATION_LIST, + variables: { id: '123' }, + }, + result: { + data: superAdminUser, + }, + }, + { + request: { + query: CREATE_SAMPLE_ORGANIZATION_MUTATION, + }, + error: new Error('Failed to create sample organization'), + }, +]; // MOCKS FOR ADMIN const MOCKS_ADMIN = [ @@ -140,6 +213,7 @@ const MOCKS_ADMIN = [ first: 8, skip: 0, filter: '', + orderBy: 'createdAt_ASC', }, notifyOnNetworkStatusChange: true, }, @@ -160,4 +234,4 @@ const MOCKS_ADMIN = [ }, ]; -export { MOCKS, MOCKS_ADMIN, MOCKS_EMPTY }; +export { MOCKS, MOCKS_ADMIN, MOCKS_EMPTY, MOCKS_WITH_ERROR }; diff --git a/src/screens/OrgPost/OrgPost.tsx b/src/screens/OrgPost/OrgPost.tsx index 3408f3f96c..f1f2e477fa 100644 --- a/src/screens/OrgPost/OrgPost.tsx +++ b/src/screens/OrgPost/OrgPost.tsx @@ -253,12 +253,9 @@ function orgPost(): JSX.Element { title="Sort Post" data-testid="sort" > - + - {t('sortPost')} + {sortingOption === 'latest' ? t('Latest') : t('Oldest')} { const calenderView = 'Calendar View'; expect(screen.queryAllByText(calenderView)).not.toBeNull(); + expect(screen.getByText('Sun')).toBeInTheDocument(); }); }); diff --git a/src/screens/UserPortal/Events/Events.tsx b/src/screens/UserPortal/Events/Events.tsx index caf1d72fd2..47332c10b4 100644 --- a/src/screens/UserPortal/Events/Events.tsx +++ b/src/screens/UserPortal/Events/Events.tsx @@ -5,7 +5,10 @@ import EventCard from 'components/UserPortal/EventCard/EventCard'; import UserSidebar from 'components/UserPortal/UserSidebar/UserSidebar'; import { Button, Dropdown, Form, InputGroup } from 'react-bootstrap'; import PaginationList from 'components/PaginationList/PaginationList'; -import { ORGANIZATION_EVENTS_CONNECTION } from 'GraphQl/Queries/Queries'; +import { + ORGANIZATION_EVENTS_CONNECTION, + ORGANIZATIONS_LIST, +} from 'GraphQl/Queries/Queries'; import { useMutation, useQuery } from '@apollo/client'; import { SearchOutlined } from '@mui/icons-material'; import styles from './Events.module.css'; @@ -18,6 +21,7 @@ import { CREATE_EVENT_MUTATION } from 'GraphQl/Mutations/mutations'; import dayjs from 'dayjs'; import { toast } from 'react-toastify'; import { errorHandler } from 'utils/errorHandler'; +import EventCalendar from 'components/EventCalendar/EventCalendar'; interface InterfaceEventCardProps { id: string; @@ -76,8 +80,15 @@ export default function events(): JSX.Element { }, }); + const { data: orgData } = useQuery(ORGANIZATIONS_LIST, { + variables: { id: organizationId }, + }); + const [create] = useMutation(CREATE_EVENT_MUTATION); + const userId = localStorage.getItem('id') as string; + const userRole = localStorage.getItem('UserType') as string; + const createEvent = async (): Promise => { try { const { data: createEventData } = await create({ @@ -109,6 +120,7 @@ export default function events(): JSX.Element { setStartTime('08:00:00'); setEndTime('10:00:00'); } + setShowCreateEventModal(false); } catch (error: any) { /* istanbul ignore next */ errorHandler(t, error); @@ -243,83 +255,95 @@ export default function events(): JSX.Element {
-
+ {mode === 0 && (
- {loading ? ( -
- Loading... -
- ) : ( - <> - {events && events.length > 0 ? ( - (rowsPerPage > 0 - ? events.slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage - ) - : /* istanbul ignore next */ - events - ).map((event: any) => { - const attendees: any = []; - event.attendees.forEach((attendee: any) => { - const r = { - id: attendee._id, +
+ {loading ? ( +
+ Loading... +
+ ) : ( + <> + {events && events.length > 0 ? ( + (rowsPerPage > 0 + ? events.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ) + : /* istanbul ignore next */ + events + ).map((event: any) => { + const attendees: any = []; + event.attendees.forEach((attendee: any) => { + const r = { + id: attendee._id, + }; + + attendees.push(r); + }); + + const creator: any = {}; + creator.firstName = event.creator.firstName; + creator.lastName = event.creator.lastName; + creator.id = event.creator._id; + + const cardProps: InterfaceEventCardProps = { + id: event._id, + title: event.title, + description: event.description, + location: event.location, + startDate: event.startDate, + endDate: event.endDate, + isRegisterable: event.isRegisterable, + isPublic: event.isPublic, + endTime: event.endTime, + startTime: event.startTime, + recurring: event.recurring, + allDay: event.allDay, + registrants: attendees, + creator, }; - attendees.push(r); - }); - - const creator: any = {}; - creator.firstName = event.creator.firstName; - creator.lastName = event.creator.lastName; - creator.id = event.creator._id; - - const cardProps: InterfaceEventCardProps = { - id: event._id, - title: event.title, - description: event.description, - location: event.location, - startDate: event.startDate, - endDate: event.endDate, - isRegisterable: event.isRegisterable, - isPublic: event.isPublic, - endTime: event.endTime, - startTime: event.startTime, - recurring: event.recurring, - allDay: event.allDay, - registrants: attendees, - creator, - }; - - return ; - }) - ) : ( - {t('nothingToShow')} - )} - - )} + return ; + }) + ) : ( + {t('nothingToShow')} + )} + + )} +
+ + + + + + +
- - - - - - -
-
+ )} + {mode === 1 && ( +
+ +
+ )}
diff --git a/src/screens/UserPortal/Home/Home.module.css b/src/screens/UserPortal/Home/Home.module.css index 48643b3445..5a092dc6f0 100644 --- a/src/screens/UserPortal/Home/Home.module.css +++ b/src/screens/UserPortal/Home/Home.module.css @@ -58,14 +58,119 @@ } .postInput { - height: 200px !important; resize: none; border: none; + outline: none; box-shadow: none; background-color: white; margin-bottom: 10px; } +.postInput:focus { + box-shadow: none; +} + .imageInput { - background-color: white; + display: none; +} + +.postContainer { + margin: 1rem auto; + background-color: transparent; + padding: 1rem; + border-radius: 4px; + /* max-width: 40rem; */ + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; +} + +.userImage { + display: flex; + width: 50px; + height: 50px; + align-items: center; + justify-content: center; + overflow: hidden; + border-radius: 50%; + position: relative; + border: 2px solid #31bb6b; +} + +.userImage img { + position: absolute; + top: 0; + left: 0; + width: 100%; + scale: 1.5; +} + +.startPostBtn { + width: 100%; + border-radius: 25px; + background-color: transparent; + /* border: 1px solid #acacac; */ + box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; + outline: none; + border: none; + color: #000; + font-weight: 900; +} + +.startPostBtn:hover { + background-color: #00000010; + border: 0; + color: #000 !important; +} + +.icons { + width: 25px; +} + +.icons svg { + stroke: #000; +} + +.icons.dark { + cursor: pointer; + border: none; + outline: none; + background-color: transparent; +} + +.icons.dark svg { + stroke: #000; +} + +.iconLabel { + margin: 0; + color: #000; + font-weight: 900; +} + +.uploadLink { + text-align: center; + width: min-content; + padding: 8px 4px; + border-radius: 4px; + cursor: pointer; +} + +.uploadLink:hover { + background-color: #00000010; +} + +.modal { + width: 100dvw; + margin: 0 auto; +} + +.previewImage { + overflow: hidden; + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 1rem; +} + +.previewImage img { + border-radius: 8px; } diff --git a/src/screens/UserPortal/Home/Home.test.tsx b/src/screens/UserPortal/Home/Home.test.tsx index 105714cfc8..aafa779255 100644 --- a/src/screens/UserPortal/Home/Home.test.tsx +++ b/src/screens/UserPortal/Home/Home.test.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import { act, render, screen } from '@testing-library/react'; +import { act, fireEvent, render, screen, within } from '@testing-library/react'; import { MockedProvider } from '@apollo/react-testing'; import { I18nextProvider } from 'react-i18next'; -import { ORGANIZATION_POST_CONNECTION_LIST } from 'GraphQl/Queries/Queries'; +import { + ORGANIZATION_POST_CONNECTION_LIST, + ADVERTISEMENTS_GET, +} from 'GraphQl/Queries/Queries'; import { BrowserRouter } from 'react-router-dom'; import { Provider } from 'react-redux'; import { store } from 'state/store'; @@ -24,6 +27,19 @@ jest.mock('react-toastify', () => ({ }, })); +const EMPTY_MOCKS = [ + { + request: { + query: ADVERTISEMENTS_GET, + }, + result: { + data: { + getAdvertisements: [], + }, + }, + }, +]; + const MOCKS = [ { request: { @@ -124,10 +140,9 @@ const MOCKS = [ request: { query: CREATE_POST_MUTATION, variables: { - title: '', - text: 'This is a test', - organizationId: '', - file: '', + title: 'Dummy Post', + text: 'This is dummy text', + organizationId: '123', }, result: { data: { @@ -138,9 +153,58 @@ const MOCKS = [ }, }, }, + { + request: { + query: ADVERTISEMENTS_GET, + variables: {}, + }, + result: { + data: { + getAdvertisements: [ + { + _id: '1234', + name: 'Ad 1', + type: 'Type 1', + orgId: 'orgId', + link: 'Link 1', + endDate: '2024-12-31', + startDate: '2022-01-01', + }, + { + _id: '2345', + name: 'Ad 2', + type: 'Type 1', + orgId: 'orgId', + link: 'Link 2', + endDate: '2024-09-31', + startDate: '2023-04-01', + }, + { + _id: '3456', + name: 'name3', + type: 'Type 2', + orgId: 'orgId', + link: 'link3', + startDate: '2023-01-30', + endDate: '2023-12-31', + }, + { + _id: '4567', + name: 'name4', + type: 'Type 2', + orgId: 'org1', + link: 'link4', + startDate: '2023-01-30', + endDate: '2023-12-01', + }, + ], + }, + }, + }, ]; const link = new StaticMockLink(MOCKS, true); +const link2 = new StaticMockLink(EMPTY_MOCKS, true); async function wait(ms = 100): Promise { await act(() => { @@ -150,6 +214,26 @@ async function wait(ms = 100): Promise { }); } +beforeEach(() => { + const url = 'http://localhost:3000/user/organization/id=orgId'; + Object.defineProperty(window, 'location', { + value: { + href: url, + }, + writable: true, + }); +}); + +let originalLocation: Location; + +beforeAll(() => { + originalLocation = window.location; +}); + +afterAll(() => { + window.location = originalLocation; +}); + describe('Testing Home Screen [User Portal]', () => { jest.mock('utils/getOrganizationId'); @@ -214,6 +298,8 @@ describe('Testing Home Screen [User Portal]', () => { expect(getOrganizationIdSpy).toHaveBeenCalled(); + userEvent.click(screen.getByTestId('startPostBtn')); + const randomPostInput = 'This is a test'; userEvent.type(screen.getByTestId('postInput'), randomPostInput); @@ -221,11 +307,7 @@ describe('Testing Home Screen [User Portal]', () => { }); test('Error toast should be visible when user tries to create a post with an empty body', async () => { - const getOrganizationIdSpy = jest - .spyOn(getOrganizationId, 'default') - .mockImplementation(() => { - return ''; - }); + const toastSpy = jest.spyOn(toast, 'error'); render( @@ -240,23 +322,14 @@ describe('Testing Home Screen [User Portal]', () => { ); await wait(); + userEvent.click(screen.getByTestId('startPostBtn')); - expect(getOrganizationIdSpy).toHaveBeenCalled(); - - userEvent.click(screen.getByTestId('postAction')); + userEvent.click(screen.getByTestId('createPostBtn')); - expect(toast.error).toBeCalledWith( - "Can't create a post with an empty body." - ); + expect(toastSpy).toBeCalledWith("Can't create a post with an empty body."); }); test('Info toast should be visible when user tries to create a post with a valid body', async () => { - const getOrganizationIdSpy = jest - .spyOn(getOrganizationId, 'default') - .mockImplementation(() => { - return ''; - }); - render( @@ -271,15 +344,125 @@ describe('Testing Home Screen [User Portal]', () => { await wait(); - expect(getOrganizationIdSpy).toHaveBeenCalled(); + userEvent.click(screen.getByTestId('startPostBtn')); const randomPostInput = 'This is a test'; userEvent.type(screen.getByTestId('postInput'), randomPostInput); expect(screen.queryByText(randomPostInput)).toBeInTheDocument(); - userEvent.click(screen.getByTestId('postAction')); + userEvent.click(screen.getByTestId('createPostBtn')); expect(toast.error).not.toBeCalledWith(); expect(toast.info).toBeCalledWith('Processing your post. Please wait.'); }); + + test('Modal should open on clicking on start a post button', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('startPostBtn')); + const startPostModal = screen.getByTestId('startPostModal'); + expect(startPostModal).toBeInTheDocument(); + }); + + test('modal closes on clicking on the close button', async () => { + render( + + + + + + + + + + ); + + await wait(); + + userEvent.click(screen.getByTestId('startPostBtn')); + const modalHeader = screen.getByTestId('startPostModal'); + expect(modalHeader).toBeInTheDocument(); + + userEvent.type(screen.getByTestId('postInput'), 'some content'); + userEvent.upload( + screen.getByTestId('postImageInput'), + new File(['image content'], 'image.png', { type: 'image/png' }) + ); + + // Check that the content and image have been added + expect(screen.getByTestId('postInput')).toHaveValue('some content'); + await screen.findByAltText('Post Image Preview'); + expect(screen.getByAltText('Post Image Preview')).toBeInTheDocument(); + + const closeButton = within(modalHeader).getByRole('button', { + name: /close/i, + }); + userEvent.click(closeButton); + + const closedModalText = screen.queryByText(/somethingOnYourMind/i); + expect(closedModalText).not.toBeInTheDocument(); + + expect(screen.getByTestId('postInput')).toHaveValue(''); + expect(screen.getByTestId('postImageInput')).toHaveValue(''); + }); + + test('triggers file input when the icon is clicked', () => { + const clickSpy = jest.spyOn(HTMLInputElement.prototype, 'click'); + + render( + + + + + + + + + + ); + + userEvent.click(screen.getByTestId('startPostBtn')); + + // Check if the file input is hidden initially + const postImageInput = screen.getByTestId('postImageInput'); + expect(postImageInput).toHaveAttribute('type', 'file'); + expect(postImageInput).toHaveStyle({ display: 'none' }); + + // Trigger icon click event + const iconButton = screen.getByTestId('addMediaBtn'); + fireEvent.click(iconButton); + + // Check if the file input is triggered to open + expect(clickSpy).toHaveBeenCalled(); + clickSpy.mockRestore(); + }); + + test('promoted post is not rendered if there is no ad content', () => { + render( + + + + + + + + + + ); + + expect(screen.queryByText('Ad 1')).not.toBeInTheDocument(); + expect(screen.queryByText('Ad 2')).not.toBeInTheDocument(); + }); }); diff --git a/src/screens/UserPortal/Home/Home.tsx b/src/screens/UserPortal/Home/Home.tsx index ad02e1b26c..9c046ff9a6 100644 --- a/src/screens/UserPortal/Home/Home.tsx +++ b/src/screens/UserPortal/Home/Home.tsx @@ -1,19 +1,27 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import type { ChangeEvent } from 'react'; import OrganizationNavbar from 'components/UserPortal/OrganizationNavbar/OrganizationNavbar'; import styles from './Home.module.css'; import UserSidebar from 'components/UserPortal/UserSidebar/UserSidebar'; import OrganizationSidebar from 'components/UserPortal/OrganizationSidebar/OrganizationSidebar'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; -import { Button, FloatingLabel, Form } from 'react-bootstrap'; +import { + Button, + Form, + Col, + Container, + Image, + Row, + Modal, +} from 'react-bootstrap'; import { Link } from 'react-router-dom'; import getOrganizationId from 'utils/getOrganizationId'; -import SendIcon from '@mui/icons-material/Send'; import PostCard from 'components/UserPortal/PostCard/PostCard'; import { useMutation, useQuery } from '@apollo/client'; import { ADVERTISEMENTS_GET, ORGANIZATION_POST_CONNECTION_LIST, + USER_DETAILS, } from 'GraphQl/Queries/Queries'; import { CREATE_POST_MUTATION } from 'GraphQl/Mutations/mutations'; import { errorHandler } from 'utils/errorHandler'; @@ -22,6 +30,7 @@ import convertToBase64 from 'utils/convertToBase64'; import { toast } from 'react-toastify'; import HourglassBottomIcon from '@mui/icons-material/HourglassBottom'; import PromotedPost from 'components/UserPortal/PromotedPost/PromotedPost'; +import UserDefault from '../../../assets/images/defaultImg.png'; interface InterfacePostCardProps { id: string; @@ -57,15 +66,28 @@ interface InterfacePostCardProps { }[]; } +interface InterfaceAdContent { + _id: string; + name: string; + type: string; + orgId: string; + link: string; + endDate: string; + startDate: string; +} + export default function home(): JSX.Element { const { t } = useTranslation('translation', { keyPrefix: 'home' }); const organizationId = getOrganizationId(window.location.href); const [posts, setPosts] = React.useState([]); - const [postContent, setPostContent] = React.useState(''); - const [postImage, setPostImage] = React.useState(''); + const [postContent, setPostContent] = React.useState(''); + const [postImage, setPostImage] = React.useState(''); + const [adContent, setAdContent] = React.useState([]); + const [filteredAd, setFilteredAd] = useState([]); + const [showStartPost, setShowStartPost] = useState(false); + const fileInputRef = useRef(null); const currentOrgId = window.location.href.split('/id=')[1] + ''; - const [adContent, setAdContent] = React.useState([]); const navbarProps = { currentPage: 'home', @@ -85,6 +107,12 @@ export default function home(): JSX.Element { variables: { id: organizationId }, }); + const userId: string | null = localStorage.getItem('userId'); + + const { data: userData } = useQuery(USER_DETAILS, { + variables: { id: userId }, + }); + const [create] = useMutation(CREATE_POST_MUTATION); const handlePost = async (): Promise => { @@ -109,6 +137,7 @@ export default function home(): JSX.Element { refetch(); setPostContent(''); setPostImage(''); + setShowStartPost(false); } } catch (error: any) { /* istanbul ignore next */ @@ -134,55 +163,133 @@ export default function home(): JSX.Element { } }, [data]); + useEffect(() => { + setFilteredAd(filterAdContent(adContent, currentOrgId)); + }, [adContent]); + + const filterAdContent = ( + adCont: InterfaceAdContent[], + currentOrgId: string, + currentDate: Date = new Date() + ): InterfaceAdContent[] => { + return adCont.filter( + (ad: InterfaceAdContent) => + ad.orgId === currentOrgId && new Date(ad.endDate) > currentDate + ); + }; + + const handlePostButtonClick = (): void => { + setShowStartPost(true); + }; + + const handleIconClick = (e: React.MouseEvent): void => { + e.preventDefault(); + + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleModalClose = (): void => { + setPostContent(''); + setPostImage(''); + setShowStartPost(false); + }; + return ( <>
-
- - - -
- => { - const target = e.target as HTMLInputElement; - const file = target.files && target.files[0]; - if (file) { - const image = await convertToBase64(file); - setPostImage(image); - } - } - } - /> - -
-
+ + + + + + + + + + + +
+
+ + + +
+ +

{t('media')}

+
+ + +
+
+ + + +
+ +

{t('event')}

+
+ + +
+
+ + + +
+ +

{t('article')}

+
+ +
+
- {adContent - .filter((ad: any) => ad.orgId == currentOrgId) - .filter((ad: any) => new Date(ad.endDate) > new Date()).length == 0 - ? '' - : adContent - .filter((ad: any) => ad.orgId == currentOrgId) - .filter((ad: any) => new Date(ad.endDate) > new Date()) - .map((post: any) => ( - - ))} + {filteredAd.length === 0 ? ( + '' + ) : ( +
+ {filteredAd.map((post: any) => ( + + ))} +
+ )} {loadingPosts ? (
Loading... @@ -285,6 +392,107 @@ export default function home(): JSX.Element { )}
+ + + + + + + + {`${userData?.user?.firstName} ${userData?.user?.lastName}`} + + + + + + + + ): Promise => { + const file = e.target.files && e.target.files[0]; + if (file) { + const image = await convertToBase64(file); + setPostImage(image); + } + }} + /> + {postImage && ( +
+ Post Image Preview +
+ )} +
+ +
+
+ + + + +
); diff --git a/src/screens/UserPortal/Settings/Settings.module.css b/src/screens/UserPortal/Settings/Settings.module.css index c8ca1e6091..2ac15983e2 100644 --- a/src/screens/UserPortal/Settings/Settings.module.css +++ b/src/screens/UserPortal/Settings/Settings.module.css @@ -1,7 +1,7 @@ .mainContainer { width: 50%; flex-grow: 3; - padding: 40px; + padding: 25px; max-height: 100%; overflow: auto; } @@ -10,17 +10,38 @@ height: calc(100vh - 66px); } -.content { +.cardHeader .cardTitle { + font-size: 1.2rem; + font-weight: 600; +} + +.cardHeader { + padding: 1.25rem 1rem 1rem 1rem; + border-bottom: 1px solid var(--bs-gray-200); + display: flex; + justify-content: space-between; + align-items: center; +} + +.cardBody { + padding: 1.25rem 1rem 1.5rem 1rem; display: flex; flex-direction: column; - max-width: 300px; - gap: 15px; } -.imageInput { - background-color: white; +.cardLabel { + font-weight: bold; + padding-bottom: 1px; + font-size: 14px; + color: #707070; + margin-bottom: 10px; +} + +.cardControl { + margin-bottom: 20px; } -.colorLight { - background-color: white; +.cardButton { + width: fit-content; + float: right; } diff --git a/src/screens/UserPortal/Settings/Settings.test.tsx b/src/screens/UserPortal/Settings/Settings.test.tsx index faec179acc..b096162e2d 100644 --- a/src/screens/UserPortal/Settings/Settings.test.tsx +++ b/src/screens/UserPortal/Settings/Settings.test.tsx @@ -136,4 +136,23 @@ describe('Testing Settings Screen [User Portal]', () => { userEvent.click(screen.getByTestId('updateUserBtn')); await wait(); }); + + test('Other settings card is rendered properly', async () => { + render( + + + + + + + + + + ); + + await wait(); + + expect(screen.getByText('Other Settings')).toBeInTheDocument(); + expect(screen.getByText('Change Language')).toBeInTheDocument(); + }); }); diff --git a/src/screens/UserPortal/Settings/Settings.tsx b/src/screens/UserPortal/Settings/Settings.tsx index 35afd78790..2d48994e89 100644 --- a/src/screens/UserPortal/Settings/Settings.tsx +++ b/src/screens/UserPortal/Settings/Settings.tsx @@ -3,13 +3,14 @@ import { useTranslation } from 'react-i18next'; import styles from './Settings.module.css'; import UserSidebar from 'components/UserPortal/UserSidebar/UserSidebar'; import UserNavbar from 'components/UserPortal/UserNavbar/UserNavbar'; -import { Button, Form } from 'react-bootstrap'; +import { Button, Card, Col, Form, Row } from 'react-bootstrap'; import convertToBase64 from 'utils/convertToBase64'; import { UPDATE_USER_MUTATION } from 'GraphQl/Mutations/mutations'; import { useMutation, useQuery } from '@apollo/client'; import { errorHandler } from 'utils/errorHandler'; import { toast } from 'react-toastify'; import { CHECK_AUTH } from 'GraphQl/Queries/Queries'; +import ChangeLanguageDropDown from 'components/ChangeLanguageDropdown/ChangeLanguageDropDown'; export default function settings(): JSX.Element { const { t } = useTranslation('translation', { @@ -83,60 +84,109 @@ export default function settings(): JSX.Element {

{t('profileSettings')}

-
- {t('firstName')} - - {t('lastName')} - - {t('emailAddress')} - - {t('updateImage')} - => { - const target = e.target as HTMLInputElement; - const file = target.files && target.files[0]; - if (file) { - const image = await convertToBase64(file); - setImage(image); - } - } - } - /> - -
+ + + +
+
+ {t('updateProfile')} +
+
+ + + {t('firstName')} + + + + {t('lastName')} + + + + {t('emailAddress')} + + + + {t('updateImage')} + + => { + const target = e.target as HTMLInputElement; + const file = target.files && target.files[0]; + if (file) { + const image = await convertToBase64(file); + setImage(image); + } + } + } + /> +
+ +
+
+
+ + + +
+
{t('otherSettings')}
+
+ + + {t('changeLanguage')} + + + +
+ +
diff --git a/src/screens/Users/Users.test.tsx b/src/screens/Users/Users.test.tsx index 4484518529..7879d833fb 100644 --- a/src/screens/Users/Users.test.tsx +++ b/src/screens/Users/Users.test.tsx @@ -12,10 +12,11 @@ import { store } from 'state/store'; import { StaticMockLink } from 'utils/StaticMockLink'; import i18nForTest from 'utils/i18nForTest'; import Users from './Users'; -import { EMPTY_MOCKS, MOCKS } from './UsersMocks'; +import { EMPTY_MOCKS, MOCKS, MOCKS2 } from './UsersMocks'; const link = new StaticMockLink(MOCKS, true); const link2 = new StaticMockLink(EMPTY_MOCKS, true); +const link3 = new StaticMockLink(MOCKS2, true); async function wait(ms = 100): Promise { await act(() => { @@ -121,6 +122,27 @@ describe('Testing Users screen', () => { userEvent.type(screen.getByTestId(/searchByName/i), ''); }); + test('testing search not found', async () => { + render( + + + + + + + + + + ); + + await wait(); + + const search = 'hello{enter}'; + await act(() => + userEvent.type(screen.getByTestId(/searchByName/i), search) + ); + }); + test('Testing User data is not present', async () => { render( @@ -179,7 +201,7 @@ describe('Testing Users screen', () => { ); }); - test('Testing sort Newest and oldest toggle', async () => { + test('Testing sorting functionality', async () => { await act(async () => { render( @@ -202,15 +224,96 @@ describe('Testing Users screen', () => { const inputText = screen.getByTestId('sortUsers'); fireEvent.click(inputText); - const toggleText = screen.getByTestId('newest'); + const toggleText = screen.getByTestId('oldest'); + fireEvent.click(toggleText); + + expect(searchInput).toBeInTheDocument(); + + fireEvent.click(inputText); + const toggleTite = screen.getByTestId('newest'); + fireEvent.click(toggleTite); + + expect(searchInput).toBeInTheDocument(); + }); + }); + + test('Testing filter functionality', async () => { + await act(async () => { + render( + + + + + + + + + + + ); + + await wait(); + const searchInput = screen.getByTestId('filter'); + expect(searchInput).toBeInTheDocument(); + + const inputText = screen.getByTestId('filterUsers'); + + fireEvent.click(inputText); + const toggleText = screen.getByTestId('admin'); fireEvent.click(toggleText); expect(searchInput).toBeInTheDocument(); + + fireEvent.click(inputText); + let toggleTite = screen.getByTestId('superAdmin'); + fireEvent.click(toggleTite); + + expect(searchInput).toBeInTheDocument(); + + fireEvent.click(inputText); + toggleTite = screen.getByTestId('user'); + fireEvent.click(toggleTite); + + expect(searchInput).toBeInTheDocument(); + fireEvent.click(inputText); - const toggleTite = screen.getByTestId('oldest'); + toggleTite = screen.getByTestId('cancel'); fireEvent.click(toggleTite); + + await wait(); + expect(searchInput).toBeInTheDocument(); }); }); + + test('check for rerendering', async () => { + const { rerender } = render( + + + + + + + + + + + ); + + await wait(); + rerender( + + + + + + + + + + + ); + await wait(); + }); }); diff --git a/src/screens/Users/Users.tsx b/src/screens/Users/Users.tsx index 0cdac832b5..6ec5ff9192 100644 --- a/src/screens/Users/Users.tsx +++ b/src/screens/Users/Users.tsx @@ -31,7 +31,7 @@ const Users = (): JSX.Element => { const [isLoadingMore, setIsLoadingMore] = useState(false); const [searchByName, setSearchByName] = useState(''); const [sortingOption, setSortingOption] = useState('newest'); - + const [filteringOption, setFilteringOption] = useState('cancel'); const userType = localStorage.getItem('UserType'); const loggedInUserId = localStorage.getItem('id'); @@ -68,10 +68,11 @@ const Users = (): JSX.Element => { setHasMore(false); } if (usersData && usersData.users) { - const newDisplayedUsers = sortUsers(usersData.users, sortingOption); + let newDisplayedUsers = sortUsers(usersData.users, sortingOption); + newDisplayedUsers = filterUsers(newDisplayedUsers, filteringOption); setDisplayedUsers(newDisplayedUsers); } - }, [usersData, sortingOption]); + }, [usersData, sortingOption, filteringOption]); // To clear the search when the component is unmounted useEffect(() => { @@ -179,14 +180,44 @@ const Users = (): JSX.Element => { (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ); - } else if (sortingOption === 'oldest') { + return sortedUsers; + } else { sortedUsers.sort( (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); + return sortedUsers; } + }; + + const handleFiltering = (option: string): void => { + setFilteringOption(option); + }; - return sortedUsers; + const filterUsers = ( + allUsers: InterfaceQueryUserListItem[], + filteringOption: string + ): InterfaceQueryUserListItem[] => { + const filteredUsers = [...allUsers]; + + if (filteringOption === 'cancel') { + return filteredUsers; + } else if (filteringOption === 'user') { + const output = filteredUsers.filter((user) => { + return user.userType === 'USER'; + }); + return output; + } else if (filteringOption === 'admin') { + const output = filteredUsers.filter((user) => { + return user.userType == 'ADMIN'; + }); + return output; + } else { + const output = filteredUsers.filter((user) => { + return user.userType == 'SUPERADMIN'; + }); + return output; + } }; const headerTitles: string[] = [ @@ -222,6 +253,7 @@ const Users = (): JSX.Element => { @@ -234,12 +266,9 @@ const Users = (): JSX.Element => { title="Sort Users" data-testid="sort" > - + - {t('sort')} + {sortingOption === 'newest' ? t('Newest') : t('Oldest')} { -