From a622abb5e2329869490bf8714dc93991f4d8d404 Mon Sep 17 00:00:00 2001 From: r41ph Date: Wed, 11 Sep 2024 08:55:33 +0100 Subject: [PATCH] feat: APP-201 buy credits flow step 1 (#2408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: “Ralph“ <“ralph.galan@regen.network> --- .eslintrc.js | 12 + README.md | 37 +- lerna.json | 1 - package.json | 10 +- tailwind.common.js | 1 + tailwind.css | 6 + web-auth/.env.example | 7 - web-auth/.eslintrc.js | 5 - web-auth/.gitignore | 23 - web-auth/CHANGELOG.md | 88 - web-auth/README.md | 32 - web-auth/gulpfile.js | 14 - web-auth/package.json | 56 - web-auth/public/index.html | 28 - web-auth/public/manifest.json | 15 - web-auth/public/robots.txt | 3 - web-auth/src/App.test.tsx | 20 - web-auth/src/App.tsx | 110 - web-auth/src/index.css | 14 - web-auth/src/index.tsx | 25 - web-auth/src/react-app-env.d.ts | 1 - web-auth/src/reportWebVitals.ts | 15 - web-auth/src/setupTests.ts | 5 - web-auth/tsconfig.json | 22 - .../{.eslintrc.js => .eslintrc.cjs} | 0 .../.prettierrc.cjs | 0 web-components/.prettierrc.js | 3 - web-components/auto-imports.d.ts | 21 + web-components/jest.config.js | 5 - web-components/package.json | 12 +- .../DenomIconWithCurrency.constants.ts | 16 + .../DenomIconWithCurrency.stories.tsx | 29 + .../DenomIconWithCurrency.test.tsx | 32 + .../DenomIconWithCurrency.tsx | 33 + .../PrefinanceTag/PrefinanceTag.stories.tsx | 21 + .../PrefinanceTag/PrefinanceTag.tsx | 38 + .../SupCurrencyAndAmount.stories.tsx | 20 + .../SupCurrencyAndAmount.test.tsx | 21 + .../SupCurrencyAndAmount.tsx | 20 + .../src/components/buttons/EditButtonIcon.tsx | 29 + .../components/buttons/SetMaxButton.test.tsx | 13 + .../src/components/buttons/SetMaxButton.tsx | 18 + .../src/components/buttons/button.stories.tsx | 12 + .../OrderSummaryCard.Content.tsx | 112 + .../OrderSummaryCard.Image.tsx | 20 + .../OrderSummaryCard.constants.tsx | 2 + .../OrderSummaryCard.stories.tsx | 81 + .../OrderSummaryCard.test.tsx | 80 + .../OrderSummaryCard/OrderSummaryCard.tsx | 23 + .../OrderSummaryCard.types.tsx | 28 + .../OrderSummmaryCard.RowHeader.tsx | 18 + .../cards/ProjectCard/ProjectCard.tsx | 21 +- .../src/components/cards/card.stories.tsx | 21 +- .../src/components/icons/CreditCardIcon.tsx | 28 + .../src/components/icons/CryptoIcon.tsx | 144 + .../src/components/icons/LeafIcon.tsx | 139 + .../src/components/icons/flags/USFlagIcon.tsx | 99 + .../src/components/icons/icons.stories.tsx | 6 + .../new/CustomSelect/CustomSelect.Option.tsx | 28 + .../CustomSelect/CustomSelect.Placeholder.tsx | 33 + .../new/CustomSelect/CustomSelect.stories.tsx | 46 + .../new/CustomSelect/CustomSelect.test.tsx | 119 + .../inputs/new/CustomSelect/CustomSelect.tsx | 76 + .../new/CustomSelect/CustomSelect.types.tsx | 18 + .../EditableInput/EditableInput.stories.tsx | 19 + .../new/EditableInput/EditableInput.test.tsx | 76 + .../new/EditableInput/EditableInput.tsx | 80 + .../src/components/inputs/new/Radio/Radio.tsx | 9 +- .../inputs/new/TextField/TextField.types.ts | 7 +- .../molecules/InfoCard/InfoCard.stories.tsx | 29 + .../molecules/InfoCard/InfoCard.tsx | 7 +- .../sliders/ProjectMedia.PrefinanceTag.tsx | 16 - .../src/components/sliders/ProjectMedia.tsx | 25 +- .../tooltip/InfoTooltipWithIcon.tsx | 14 +- .../tooltip/QuestionMarkTooltip.tsx | 13 +- web-components/test/setup.ts | 1 + web-components/test/test-utils.tsx | 2 + web-components/tsconfig.json | 6 +- web-components/vite.config.ts | 18 + web-marketplace/package.json | 13 +- web-marketplace/src/App.test.tsx | 53 - .../src/__snapshots__/storyshots.test.ts.snap | 5237 ----------------- .../CreditsAmount/CreditsAmount.Header.tsx | 69 + .../CreditsAmount/CreditsAmount.constants.ts | 26 + .../CreditsAmount/CreditsAmount.mock.tsx | 42 + .../CreditsAmount/CreditsAmount.stories.tsx | 70 + .../CreditsAmount/CreditsAmount.test.tsx | 134 + .../molecules/CreditsAmount/CreditsAmount.tsx | 154 + .../CreditsAmount/CreditsAmount.types.tsx | 38 + .../CreditsAmount/CreditsAmount.utils.tsx | 37 + .../molecules/CreditsAmount/CreditsInput.tsx | 78 + .../molecules/CreditsAmount/CurrencyInput.tsx | 141 + .../molecules/DenomIcon/DenomIcon.tsx | 40 +- .../src/components/molecules/Form/Form.tsx | 5 +- .../ChooseCreditsForm.AdvanceSettings.tsx | 89 + .../ChooseCreditsForm.CryptoOptions.tsx | 68 + .../ChooseCreditsForm.PaymentOptions.tsx | 93 + .../ChooseCreditsForm.constants.tsx | 10 + .../ChooseCreditsForm.schema.tsx | 43 + .../ChooseCreditsForm.stories.tsx | 23 + .../ChooseCreditsForm.test.tsx | 53 + .../ChooseCreditsForm/ChooseCreditsForm.tsx | 208 + .../ChooseCreditsForm.types.tsx | 24 + .../ChooseCreditsForm.utils.ts | 15 + .../src/config/allowedBaseDenoms.ts | 3 + web-marketplace/src/jest.mock.ts | 13 - web-marketplace/src/lib/i18n/locales/en.po | 83 +- web-marketplace/src/lib/i18n/locales/es.po | 83 +- web-marketplace/src/lib/rdf/rdf.test.ts | 4 +- web-marketplace/src/storyshots.test.ts | 20 - web-marketplace/test/setup.ts | 1 + web-marketplace/test/test-utils.tsx | 107 + web-marketplace/tsconfig.json | 15 +- web-marketplace/vite.config.mts | 8 +- web-www/package.json | 2 +- 115 files changed, 3540 insertions(+), 5919 deletions(-) delete mode 100644 web-auth/.env.example delete mode 100644 web-auth/.eslintrc.js delete mode 100644 web-auth/.gitignore delete mode 100644 web-auth/CHANGELOG.md delete mode 100644 web-auth/README.md delete mode 100644 web-auth/gulpfile.js delete mode 100644 web-auth/package.json delete mode 100644 web-auth/public/index.html delete mode 100644 web-auth/public/manifest.json delete mode 100644 web-auth/public/robots.txt delete mode 100644 web-auth/src/App.test.tsx delete mode 100644 web-auth/src/App.tsx delete mode 100644 web-auth/src/index.css delete mode 100644 web-auth/src/index.tsx delete mode 100644 web-auth/src/react-app-env.d.ts delete mode 100644 web-auth/src/reportWebVitals.ts delete mode 100644 web-auth/src/setupTests.ts delete mode 100644 web-auth/tsconfig.json rename web-components/{.eslintrc.js => .eslintrc.cjs} (100%) rename web-auth/.prettierrc.js => web-components/.prettierrc.cjs (100%) delete mode 100644 web-components/.prettierrc.js create mode 100644 web-components/auto-imports.d.ts delete mode 100644 web-components/jest.config.js create mode 100644 web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants.ts create mode 100644 web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.stories.tsx create mode 100644 web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.test.tsx create mode 100644 web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.tsx create mode 100644 web-components/src/components/PrefinanceTag/PrefinanceTag.stories.tsx create mode 100644 web-components/src/components/PrefinanceTag/PrefinanceTag.tsx create mode 100644 web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.stories.tsx create mode 100644 web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.test.tsx create mode 100644 web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.tsx create mode 100644 web-components/src/components/buttons/EditButtonIcon.tsx create mode 100644 web-components/src/components/buttons/SetMaxButton.test.tsx create mode 100644 web-components/src/components/buttons/SetMaxButton.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Content.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Image.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.constants.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.stories.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.test.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.types.tsx create mode 100644 web-components/src/components/cards/OrderSummaryCard/OrderSummmaryCard.RowHeader.tsx create mode 100644 web-components/src/components/icons/CreditCardIcon.tsx create mode 100644 web-components/src/components/icons/CryptoIcon.tsx create mode 100644 web-components/src/components/icons/LeafIcon.tsx create mode 100644 web-components/src/components/icons/flags/USFlagIcon.tsx create mode 100644 web-components/src/components/inputs/new/CustomSelect/CustomSelect.Option.tsx create mode 100644 web-components/src/components/inputs/new/CustomSelect/CustomSelect.Placeholder.tsx create mode 100644 web-components/src/components/inputs/new/CustomSelect/CustomSelect.stories.tsx create mode 100644 web-components/src/components/inputs/new/CustomSelect/CustomSelect.test.tsx create mode 100644 web-components/src/components/inputs/new/CustomSelect/CustomSelect.tsx create mode 100644 web-components/src/components/inputs/new/CustomSelect/CustomSelect.types.tsx create mode 100644 web-components/src/components/inputs/new/EditableInput/EditableInput.stories.tsx create mode 100644 web-components/src/components/inputs/new/EditableInput/EditableInput.test.tsx create mode 100644 web-components/src/components/inputs/new/EditableInput/EditableInput.tsx delete mode 100644 web-components/src/components/sliders/ProjectMedia.PrefinanceTag.tsx create mode 100644 web-components/test/setup.ts create mode 100644 web-components/test/test-utils.tsx create mode 100644 web-components/vite.config.ts delete mode 100644 web-marketplace/src/App.test.tsx delete mode 100644 web-marketplace/src/__snapshots__/storyshots.test.ts.snap create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.mock.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx create mode 100644 web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.types.tsx create mode 100644 web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.utils.ts delete mode 100644 web-marketplace/src/jest.mock.ts delete mode 100644 web-marketplace/src/storyshots.test.ts create mode 100644 web-marketplace/test/setup.ts create mode 100644 web-marketplace/test/test-utils.tsx diff --git a/.eslintrc.js b/.eslintrc.js index c6cc528d24..a50fd6ec46 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -65,5 +65,17 @@ module.exports = { 'import/no-anonymous-default-export': 'off', }, }, + { + files: [ + '*.test.ts', + '*.spec.ts', + '*.test.tsx', + '*.spec.tsx', + '*.stories.tsx', + ], + rules: { + 'lingui/no-unlocalized-strings': 'off', + }, + }, ], }; diff --git a/README.md b/README.md index 06624fee05..b903f6c428 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ The website for the [Regen Network](https://regen.network) decentralized infrast - [GraphQL Type generation](#graphql-type-generation) - [Storybook](#storybook) - [Website](#website) - - [Deploying the Custom Login form to Auth0](#deploying-the-custom-login-form-to-auth0) - [Testing](#testing) - [Code style](#code-style) - [i18n](#i18n) @@ -34,7 +33,6 @@ This project uses [bun](https://bun.sh/) with [bun workspaces](https://bun.sh/do - `web-marketplace`: Registry React application - `web-components`: React components and [material-ui](https://material-ui.com/) custom theme - `web-storybook`: [Storybook](https://storybook.js.org/) config -- `web-auth`: React application used for Auth0 Custom Universal Login [Lerna](https://github.com/lerna/lerna) is also used to bump packages versions and push new releases. @@ -62,7 +60,6 @@ bun install Set variables in `.env` files in `web-marketplace/` and `web-storybook/` folders based on provided `.env.example` files. -For `web-auth`, follow these [setup instructions](web-auth/README.md#setup). ## Development @@ -136,31 +133,29 @@ bun run build-storybook bun run build-www ``` -### Deploying the Custom Login form to Auth0 +## Testing -Please, follow [these instructions](web-auth/README.md#setup) and then: +#### - Running tests +We are using [Vitest](https://vitest.dev/) as a test runner. -1. Run `bun run build-auth` command. -2. Copy the code from `./build/index.html`. -3. Paste it into the Universal Login HTML form from [Auth dashboard](https://manage.auth0.com/dashboard/us/regen-network-registry/login_page) and save. +Tests can be run in the terminal with the following commands from the project root: -This could be automated in the future. +* To run `web-marketplace` tests + ```sh + bun run test-marketplace + ``` -## Testing +* To run `web-components` tests + ```sh + bun run test-components + ``` +In both cases the test runner is launched in the interactive watch mode. -```sh -bun run test -``` +#### - Writing tests +When writing test in `web-marketplace` remember to import the methods from`'web-marketplace/test/test-utils'`, specially the `render` method, as it is a custom render that wraps components with the necessary providers. -Launches the test runner in the interactive watch mode. -[Jest](https://jestjs.io/) is used as test runner. +In `web-components`, methods should be imported directly from `'@testing-library/*'` -We're using [StoryShots](https://storybook.js.org/docs/testing/structural-testing/#using-storyshots) for snapshots testing. -Update web-components snapshots: - -```sh -bun run test-update-snapshot -``` ## Code style diff --git a/lerna.json b/lerna.json index 4ccd2af3e7..572637aaf0 100644 --- a/lerna.json +++ b/lerna.json @@ -6,7 +6,6 @@ "web-marketplace", "web-components", "web-storybook", - "web-auth", "web-www" ] } diff --git a/package.json b/package.json index c03166831e..1998c4f9d6 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,6 @@ "web-marketplace", "web-components", "web-storybook", - "web-auth", "web-www" ], "scripts": { @@ -22,16 +21,14 @@ "build-www": "bun run --cwd web-www build", "build-components": "bun run --cwd web-components build", "build-storybook": "bun run --cwd web-storybook build-storybook", - "build-auth": "bun run --cwd web-auth build", "clean": "find . -iname 'node_modules' -maxdepth 2 | xargs rm -rf", "format": "bun run --cwd web-components format & bun run --cwd web-marketplace format & bun run --cwd web-www format", "lint": "bun run --cwd web-components lint & bun run --cwd web-marketplace lint & bun run --cwd web-www lint", "format-and-fix": "bun run --cwd web-components format-and-fix & bun run --cwd web-marketplace format-and-fix & bun run --cwd web-www format-and-fix", "storybook": "bun run --cwd web-storybook storybook", - "test": "bun run --cwd web-marketplace test-no-watch", + "test-marketplace": "bun run --cwd web-marketplace test", "test-components": "bun run --cwd web-components test", "start": "bun run --cwd web-marketplace start", - "start-auth": "bun run --cwd web-auth start", "watch": "bun run --cwd web-components watch", "test-update-snapshot": "bun run --cwd web-marketplace test-update-snapshot", "bump": "lerna version --no-push --conventional-commits", @@ -46,7 +43,6 @@ "@graphql-codegen/typescript-operations": "^1.18.0", "@graphql-codegen/typescript-react-apollo": "2.2.4", "@types/css-mediaquery": "^0.1.1", - "@types/jest": "27.0.3", "@types/node": "13.1.1", "babel-loader": "8.1.0", "css-mediaquery": "0.1.2", @@ -59,13 +55,13 @@ "prettier": "2.4.1", "shx": "^0.3.2", "storybook": "^7.3.2", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "unplugin-auto-import": "^0.18.2" }, "resolutions": { "eslint": "^8.42.0", "formik-mui": "^5.0.0-alpha.0", "formik": "2.2.9", - "@types/jest": "^29.0.0", "@types/react": "^18.0.23", "@types/react-dom": "^18.0.7", "babel-eslint": "^10.1.0", diff --git a/tailwind.common.js b/tailwind.common.js index b1f8e93dac..8ea7fcd57e 100644 --- a/tailwind.common.js +++ b/tailwind.common.js @@ -4,6 +4,7 @@ module.exports = { theme: { fontFamily: { sans: ['"Lato"', '-apple-system', 'sans-serif'], + muli: ['"Muli"', '-apple-system', 'sans-serif'], }, colors: { // Make sure these guidelines are followed when adding new colors: https://tailwindcss.com/docs/customizing-colors#using-css-variables diff --git a/tailwind.css b/tailwind.css index 462f57cbdb..5404a33e55 100644 --- a/tailwind.css +++ b/tailwind.css @@ -47,6 +47,7 @@ --purple-300: 128 142 171; --purple-200: 187 195 210; --purple-100: 221 225 233; + } .dark { @@ -86,4 +87,9 @@ --purple-200: 128 142 171; --purple-100: 86 104 143; } + + .bg-transparent { + background-color: transparent; + } + } diff --git a/web-auth/.env.example b/web-auth/.env.example deleted file mode 100644 index 0198cd3144..0000000000 --- a/web-auth/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -INLINE_RUNTIME_CHUNK=false -GENERATE_SOURCEMAP=false -SKIP_PREFLIGHT_CHECK=true -REACT_APP_API_URI= -REACT_APP_AUTH0_DOMAIN=regen-network-registry.auth0.com -REACT_APP_SIGNUP_LINK=https://app.regen.network/signup -REACT_APP_RECAPTCHAV3_SITE_KEY= diff --git a/web-auth/.eslintrc.js b/web-auth/.eslintrc.js deleted file mode 100644 index 55b76a1c47..0000000000 --- a/web-auth/.eslintrc.js +++ /dev/null @@ -1,5 +0,0 @@ -const baseConfig = require('../.eslintrc'); - -module.exports = { - ...baseConfig, -}; diff --git a/web-auth/.gitignore b/web-auth/.gitignore deleted file mode 100644 index 4d29575de8..0000000000 --- a/web-auth/.gitignore +++ /dev/null @@ -1,23 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* diff --git a/web-auth/CHANGELOG.md b/web-auth/CHANGELOG.md deleted file mode 100644 index e31e7f0cc9..0000000000 --- a/web-auth/CHANGELOG.md +++ /dev/null @@ -1,88 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [2.6.5](https://github.com/regen-network/regen-web/compare/v2.6.4...v2.6.5) (2024-08-16) - -**Note:** Version bump only for package web-auth - -# [2.6.0](https://github.com/regen-network/regen-web/compare/v2.5.1...v2.6.0) (2024-05-30) - -**Note:** Version bump only for package web-auth - -# [2.4.0](https://github.com/regen-network/regen-web/compare/v2.3.0...v2.4.0) (2024-04-24) - -**Note:** Version bump only for package web-auth - -# [2.3.0](https://github.com/regen-network/regen-web/compare/v2.2.1...v2.3.0) (2024-04-09) - -**Note:** Version bump only for package web-auth - -## [2.2.1](https://github.com/regen-network/regen-web/compare/v2.2.0...v2.2.1) (2024-03-06) - -**Note:** Version bump only for package web-auth - -# [2.2.0](https://github.com/regen-network/regen-web/compare/v2.1.1...v2.2.0) (2024-03-05) - -**Note:** Version bump only for package web-auth - -## [2.1.1](https://github.com/regen-network/regen-web/compare/v2.1.0...v2.1.1) (2024-02-27) - -**Note:** Version bump only for package web-auth - -# [2.1.0](https://github.com/regen-network/regen-web/compare/v2.0.0...v2.1.0) (2024-02-27) - -**Note:** Version bump only for package web-auth - -# [2.0.0](https://github.com/regen-network/regen-web/compare/v1.14.1...v2.0.0) (2024-02-20) - -**Note:** Version bump only for package web-auth - -# [1.14.0](https://github.com/regen-network/regen-web/compare/v1.13.0...v1.14.0) (2023-10-23) - -**Note:** Version bump only for package web-auth - -# [1.13.0](https://github.com/regen-network/regen-web/compare/v1.12.2...v1.13.0) (2023-09-27) - -**Note:** Version bump only for package web-auth - -# [1.12.0](https://github.com/regen-network/regen-web/compare/v1.11.0...v1.12.0) (2023-09-19) - -**Note:** Version bump only for package web-auth - -# [1.11.0](https://github.com/regen-network/regen-web/compare/v1.10.0...v1.11.0) (2023-08-09) - -**Note:** Version bump only for package web-auth - -# [1.10.0](https://github.com/regen-network/regen-web/compare/v1.9.0...v1.10.0) (2023-07-20) - -**Note:** Version bump only for package web-auth - -# [1.9.0](https://github.com/regen-network/regen-web/compare/v1.8.2...v1.9.0) (2023-07-11) - -**Note:** Version bump only for package web-auth - -## [1.8.2](https://github.com/regen-network/regen-web/compare/v1.8.1...v1.8.2) (2023-07-06) - -**Note:** Version bump only for package web-auth - -# [1.8.0](https://github.com/regen-network/regen-web/compare/v1.7.0...v1.8.0) (2023-06-13) - -**Note:** Version bump only for package web-auth - -# [1.7.0](https://github.com/regen-network/regen-web/compare/v1.6.0...v1.7.0) (2023-06-06) - -**Note:** Version bump only for package web-auth - -# [1.5.0](https://github.com/regen-network/regen-web/compare/v1.4.6...v1.5.0) (2023-05-09) - -**Note:** Version bump only for package web-auth - -# [1.3.0](https://github.com/regen-network/regen-web/compare/v1.2.3...v1.3.0) (2023-04-05) - -**Note:** Version bump only for package web-auth - -# [1.2.0](https://github.com/regen-network/regen-web/compare/v1.1.0...v1.2.0) (2023-02-27) - -**Note:** Version bump only for package web-auth diff --git a/web-auth/README.md b/web-auth/README.md deleted file mode 100644 index cac9c96cc2..0000000000 --- a/web-auth/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Regen Registry Log In - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). -It displays a login form to be used as custom [Auth0 Universal Login](https://auth0.com/docs/universal-login) UI. It's only meant to be run locally or from Auth0. - -## Setup - -Make sure to follow these [installation instructions](../README.md#installation) first. - -Based on `.env.example`, create one `.env.development.local` and `.env.production.local` with appropriate values. - -## Available Scripts - -In the project directory, you can run: - -### `yarn start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `yarn test` - -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. - -### `yarn build` - -Builds the app for production to the `build` folder. -Uses [gulp](https://gulpjs.com/) to bundle the app into a single `./build/index.html` file. \ No newline at end of file diff --git a/web-auth/gulpfile.js b/web-auth/gulpfile.js deleted file mode 100644 index d282f5dadf..0000000000 --- a/web-auth/gulpfile.js +++ /dev/null @@ -1,14 +0,0 @@ -const gulp = require('gulp'); -const inlinesource = require('gulp-inline-source'); -const replace = require('gulp-replace'); - -gulp.task('default', () => { - return gulp.src('./build/*.html') - .pipe(replace('.js">', '.js" inline>')) - .pipe(replace('rel="stylesheet">', 'rel="stylesheet" inline>')) - .pipe(inlinesource({ - compress: false, - ignore: ['png'] - })) - .pipe(gulp.dest('./build')); -}); \ No newline at end of file diff --git a/web-auth/package.json b/web-auth/package.json deleted file mode 100644 index 87831046ae..0000000000 --- a/web-auth/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "web-auth", - "homepage": ".", - "version": "2.6.5", - "private": true, - "dependencies": { - "@mui/styles": "^5.10.10", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^14.4.3", - "@types/jest": "^26.0.15", - "@types/node": "^12.0.0", - "@types/react": "17.0.2", - "@types/react-dom": "17.0.2", - "auth0-js": "^9.18.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-google-recaptcha-v3": "^1.8.0", - "react-scripts": "4.0.3", - "tss-react": "^4.4.4", - "web-components": "workspace:*", - "web-vitals": "^1.0.1" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build && gulp", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-simple-import-sort": "^7.0.0", - "gulp": "^4.0.2", - "gulp-inline-source": "^4.0.0", - "gulp-replace": "^1.0.0", - "storybook-addon-react-router-v6": "^0.1.10" - } -} diff --git a/web-auth/public/index.html b/web-auth/public/index.html deleted file mode 100644 index 7c64c8248a..0000000000 --- a/web-auth/public/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - Regen Registry | Log in - - - -
- - - diff --git a/web-auth/public/manifest.json b/web-auth/public/manifest.json deleted file mode 100644 index 22627e1f5a..0000000000 --- a/web-auth/public/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "short_name": "Regen Registry | Log in", - "name": "Regen Registry | Log in", - "icons": [ - { - "src": "https://regen-registry.s3.amazonaws.com/favicon/favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#000000", - "background_color": "#ffffff" -} diff --git a/web-auth/public/robots.txt b/web-auth/public/robots.txt deleted file mode 100644 index e9e57dc4d4..0000000000 --- a/web-auth/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/web-auth/src/App.test.tsx b/web-auth/src/App.test.tsx deleted file mode 100644 index bc2487e18a..0000000000 --- a/web-auth/src/App.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { MockedProvider } from '@apollo/client/testing'; -import ReactDOM, { unmountComponentAtNode } from 'react-dom'; -import ThemeProvider from 'web-components/src/theme/RegenThemeProvider'; - -import App from './App'; - -it('renders without crashing', () => { - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render( - - - - - , - , - container, - ); - unmountComponentAtNode(container); -}); diff --git a/web-auth/src/App.tsx b/web-auth/src/App.tsx deleted file mode 100644 index df0ed23b1d..0000000000 --- a/web-auth/src/App.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { WebAuth } from 'auth0-js'; -import axios from 'axios'; -import React, { useCallback } from 'react'; -import { useGoogleReCaptcha } from 'react-google-recaptcha-v3'; -import { makeStyles } from 'tss-react/mui'; -import LoginForm, { - Values, -} from 'web-components/src/components/form/LoginForm'; -import OnBoardingSection from 'web-components/src/components/section/OnBoardingSection'; -import { Theme } from 'web-components/src/theme/muiTheme'; - -const searchParams = new URLSearchParams(window.location.search); -const state = searchParams.get('state') || undefined; -const clientID = searchParams.get('client') || undefined; -const redirectUri = searchParams.get('redirect_uri') || undefined; -const audience = searchParams.get('audience') || undefined; -const nonce = searchParams.get('nonce') || undefined; -const scope = searchParams.get('nonce') || undefined; -const responseType = searchParams.get('response_type') || 'code'; -const responseMode = searchParams.get('response_mode') || undefined; - -const auth0 = new WebAuth({ - domain: process.env.REACT_APP_AUTH0_DOMAIN || window.location.host, - clientID: clientID || 'rEuc1WLPAQVXZ7gJrWg4AL9EhWMHmLu8', - audience: audience, - redirectUri, -}); - -const useStyles = makeStyles()((theme: Theme) => ({ - logo: { - display: 'flex', - [theme.breakpoints.up('sm')]: { - margin: `${theme.spacing(11.25)} auto 0`, - }, - [theme.breakpoints.down('sm')]: { - margin: `${theme.spacing(7)} auto 0`, - }, - }, -})); - -function App(): JSX.Element { - const { classes } = useStyles(); - const { executeRecaptcha } = useGoogleReCaptcha(); - - const submit = useCallback( - ({ email, password }: Values): Promise => { - return new Promise(async (resolve, reject) => { - try { - const token = await executeRecaptcha?.('login_page'); - const apiUri: string = - process.env.REACT_APP_API_URI || 'http://localhost:5000'; - const res = await axios({ - method: 'POST', - url: `${apiUri}/recaptcha/v3/verify`, - data: { - userResponse: token, - }, - }); - if (res?.data?.success && res.data.score >= 0.5) { - auth0.login( - { - realm: 'Username-Password-Authentication', - email, - password, - nonce, - scope, - responseType, - responseMode, - state, - }, - function (err) { - if (err) { - reject(err); - } else { - resolve(); - } - }, - ); - } else { - reject(); - } - } catch (err) { - reject(err); - } - }); - }, - [executeRecaptcha], - ); - - return ( - <> - Regen Network logo - - - - - ); -} - -export default App; diff --git a/web-auth/src/index.css b/web-auth/src/index.css deleted file mode 100644 index 64a1f62d4a..0000000000 --- a/web-auth/src/index.css +++ /dev/null @@ -1,14 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - background-color: #fafafa; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/web-auth/src/index.tsx b/web-auth/src/index.tsx deleted file mode 100644 index 86e6cedce2..0000000000 --- a/web-auth/src/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import './index.css'; - -import CssBaseline from '@mui/material/CssBaseline'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3'; -import ThemeProvider from 'web-components/src/theme/RegenThemeProvider'; - -import App from './App'; -import reportWebVitals from './reportWebVitals'; - -ReactDOM.render( - - - - - - , - document.getElementById('root'), -); - -// If you want to start measuring performance in your app, pass a function -// to log results (for example: reportWebVitals(consolle.log)) -// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals -reportWebVitals(); diff --git a/web-auth/src/react-app-env.d.ts b/web-auth/src/react-app-env.d.ts deleted file mode 100644 index 6431bc5fc6..0000000000 --- a/web-auth/src/react-app-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/web-auth/src/reportWebVitals.ts b/web-auth/src/reportWebVitals.ts deleted file mode 100644 index eb4be08f20..0000000000 --- a/web-auth/src/reportWebVitals.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ReportHandler } from 'web-vitals'; - -const reportWebVitals = (onPerfEntry?: ReportHandler): void => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/web-auth/src/setupTests.ts b/web-auth/src/setupTests.ts deleted file mode 100644 index 8f2609b7b3..0000000000 --- a/web-auth/src/setupTests.ts +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/web-auth/tsconfig.json b/web-auth/tsconfig.json deleted file mode 100644 index 6c758f7dcd..0000000000 --- a/web-auth/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "allowSyntheticDefaultImports": true, - "isolatedModules": true, - "outDir": "build", - "resolveJsonModule": true, - "rootDir": "src", - "allowJs": true, - "jsx": "react-jsx", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], - "module": "esnext", - "noEmit": true - }, - "include": [ - "src" - ] -} diff --git a/web-components/.eslintrc.js b/web-components/.eslintrc.cjs similarity index 100% rename from web-components/.eslintrc.js rename to web-components/.eslintrc.cjs diff --git a/web-auth/.prettierrc.js b/web-components/.prettierrc.cjs similarity index 100% rename from web-auth/.prettierrc.js rename to web-components/.prettierrc.cjs diff --git a/web-components/.prettierrc.js b/web-components/.prettierrc.js deleted file mode 100644 index 5124a322b3..0000000000 --- a/web-components/.prettierrc.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - ...require('../.prettierrc'), -}; \ No newline at end of file diff --git a/web-components/auto-imports.d.ts b/web-components/auto-imports.d.ts new file mode 100644 index 0000000000..13dd447e32 --- /dev/null +++ b/web-components/auto-imports.d.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const afterAll: typeof import('vitest')['afterAll'] + const afterEach: typeof import('vitest')['afterEach'] + const assert: typeof import('vitest')['assert'] + const beforeAll: typeof import('vitest')['beforeAll'] + const beforeEach: typeof import('vitest')['beforeEach'] + const chai: typeof import('vitest')['chai'] + const describe: typeof import('vitest')['describe'] + const expect: typeof import('vitest')['expect'] + const it: typeof import('vitest')['it'] + const suite: typeof import('vitest')['suite'] + const test: typeof import('vitest')['test'] + const vi: typeof import('vitest')['vi'] + const vitest: typeof import('vitest')['vitest'] +} diff --git a/web-components/jest.config.js b/web-components/jest.config.js deleted file mode 100644 index e86e13bab9..0000000000 --- a/web-components/jest.config.js +++ /dev/null @@ -1,5 +0,0 @@ -/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; diff --git a/web-components/package.json b/web-components/package.json index 6f03a67f8f..6ce8956432 100644 --- a/web-components/package.json +++ b/web-components/package.json @@ -3,6 +3,7 @@ "version": "2.6.5", "private": true, "main": "build/index.js", + "type": "module", "scripts": { "remove-previous-files": "shx rm -rf ./lib/* ./tsconfig.tsbuildinfo", "copy-assets": "cp ./src/theme/fonts.css ./lib/theme/fonts.css && cp -R ./src/theme/assets ./lib/theme/", @@ -12,7 +13,8 @@ "lint-fix": "eslint ./src --fix -c .eslintrc.js --ext .ts,.tsx", "format": "prettier --write --loglevel warn --ignore-path=../.prettierignore './src/**/*.{ts,tsx,json,md,css}'", "format-and-fix": "bun run format && bun run lint-fix", - "test": "jest" + "test": "vitest", + "test:ui": "vitest --ui" }, "browserslist": [ ">0.2%", @@ -86,17 +88,19 @@ "zxcvbn": "^4.4.2" }, "devDependencies": { - "@types/jest": "^29.0.0", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^16.0.0", "@types/mapbox__mapbox-sdk": "^0.11.1", "@types/react-lazyload": "^3.1.0", + "canvas": "^2.11.2", "clsx": "^2.0.0", "csstype": "^3.1.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-simple-import-sort": "^7.0.0", - "jest": "^28.1.3", "prettier": "2.4.1", "storybook-addon-react-router-v6": "^0.1.10", "tailwind-merge": "^1.14.0", - "ts-jest": "^28.0.8" + "vite": "^5.3.5", + "vitest": "^2.0.2" } } diff --git a/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants.ts b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants.ts new file mode 100644 index 0000000000..d142baf045 --- /dev/null +++ b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants.ts @@ -0,0 +1,16 @@ +import { + REGEN_DENOM, + USD_DENOM, + USDC_DENOM, + USDCAXL_DENOM, +} from 'web-marketplace/src/config/allowedBaseDenoms'; + +export const CURRENCIES = { + usd: USD_DENOM, + usdc: USDC_DENOM, + uregen: REGEN_DENOM, + usdcaxl: USDCAXL_DENOM, +} as const; + +export type Currency = keyof typeof CURRENCIES; +export type CryptoCurrencies = Exclude; diff --git a/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.stories.tsx b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.stories.tsx new file mode 100644 index 0000000000..f2900786a6 --- /dev/null +++ b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { DenomIconWithCurrency } from './DenomIconWithCurrency'; +import { CURRENCIES } from './DenomIconWithCurrency.constants'; + +export default { + title: 'DenomIconWithCurrency', + component: DenomIconWithCurrency, +} as Meta; + +type Story = StoryObj; + +export const IconAndCurrency: Story = { + render: args => , +}; + +IconAndCurrency.args = { + currency: CURRENCIES.usd, +}; + +export const withTooltip: Story = { + render: args => , +}; + +withTooltip.args = { + currency: CURRENCIES.uregen, + tooltipText: + 'Different sellers may sell the same credits at different prices. We automatically choose the lowest priced credits for you. This price is the average price of all the credits in your cart.', +}; diff --git a/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.test.tsx b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.test.tsx new file mode 100644 index 0000000000..478fd5b5a2 --- /dev/null +++ b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.test.tsx @@ -0,0 +1,32 @@ +import { render } from '@testing-library/react'; +import { fireEvent, screen } from 'web-components/test/test-utils'; +import { USD_DENOM } from 'web-marketplace/src/config/allowedBaseDenoms'; + +import { DenomIconWithCurrency } from './DenomIconWithCurrency'; + +describe('DenomIconWithCurrency', () => { + const currency = USD_DENOM; + + it('renders the denom icon and currency code', () => { + render(); + + const flagIcon = screen.getByTestId('USFlagIcon'); + const currencyCode = screen.getByText(currency.toUpperCase()); + + expect(flagIcon).toBeInTheDocument(); + expect(currencyCode).toBeInTheDocument(); + }); + + it('renders info icon and tooltip', async () => { + render( + , + ); + + const tooltipIcon = screen.getByTestId('question-mark-tooltip'); + expect(tooltipIcon).toBeInTheDocument(); + + fireEvent.mouseEnter(tooltipIcon); + const currencyCode = await screen.findByText(/tooltip text/i); + expect(currencyCode).toBeInTheDocument(); + }); +}); diff --git a/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.tsx b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.tsx new file mode 100644 index 0000000000..3e1121fb88 --- /dev/null +++ b/web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.tsx @@ -0,0 +1,33 @@ +import { cn } from 'web-components/src/utils/styles/cn'; +import { DenomIcon } from 'web-marketplace/src/components/molecules/DenomIcon/DenomIcon'; + +import QuestionMarkTooltip from '../tooltip/QuestionMarkTooltip'; +import { Body } from '../typography'; +import { Currency } from './DenomIconWithCurrency.constants'; + +export function DenomIconWithCurrency({ + currency, + className, + tooltipText, +}: { + currency: Currency; + className?: string; + tooltipText?: string; +}) { + return ( + + + {currency.toUpperCase()} + {tooltipText && ( + + )} + + ); +} diff --git a/web-components/src/components/PrefinanceTag/PrefinanceTag.stories.tsx b/web-components/src/components/PrefinanceTag/PrefinanceTag.stories.tsx new file mode 100644 index 0000000000..0dfcb1a563 --- /dev/null +++ b/web-components/src/components/PrefinanceTag/PrefinanceTag.stories.tsx @@ -0,0 +1,21 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { PrefinanceTag } from './PrefinanceTag'; + +export default { + title: 'PrefinanceTag', + component: PrefinanceTag, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: args => , +}; + +Default.args = { + classNames: { + root: '', + label: '', + }, +}; diff --git a/web-components/src/components/PrefinanceTag/PrefinanceTag.tsx b/web-components/src/components/PrefinanceTag/PrefinanceTag.tsx new file mode 100644 index 0000000000..3d24b2046e --- /dev/null +++ b/web-components/src/components/PrefinanceTag/PrefinanceTag.tsx @@ -0,0 +1,38 @@ +import { cn } from 'web-components/src/utils/styles/cn'; + +import { PREFINANCE } from '../cards/ProjectCard/ProjectCard.constants'; +import { PrefinanceIcon } from '../icons/PrefinanceIcon'; +import { Label } from '../typography'; + +export const PrefinanceTag = ({ + classNames = { + root: '', + label: '', + }, + iconSize = { + width: '18', + height: '19', + }, +}: { + classNames?: { root?: string; label?: string }; + iconSize?: { + width: string; + height: string; + }; +}) => ( +
+ + +
+); diff --git a/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.stories.tsx b/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.stories.tsx new file mode 100644 index 0000000000..3acc4e6f32 --- /dev/null +++ b/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.stories.tsx @@ -0,0 +1,20 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { CURRENCIES } from '../DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import { SupCurrencyAndAmount } from './SupCurrencyAndAmount'; + +export default { + title: 'SupCurrencyAndAmount', + component: SupCurrencyAndAmount, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: args => , +}; + +Default.args = { + price: 5, + currencyCode: CURRENCIES.usd, +}; diff --git a/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.test.tsx b/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.test.tsx new file mode 100644 index 0000000000..2a02cb1c64 --- /dev/null +++ b/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.test.tsx @@ -0,0 +1,21 @@ +import { render } from '@testing-library/react'; +import { SupCurrencyAndAmount } from 'web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount'; + +import { CURRENCIES } from '../DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +describe('SupCurrencyAndAmount', () => { + it('renders the currency symbol and amount', () => { + const currency = CURRENCIES.usd; + const amount = '100.00'; + + const { getByText } = render( + , + ); + + const currencySymbol = getByText('$'); + const amountText = getByText(amount); + + expect(currencySymbol).toBeInTheDocument(); + expect(amountText).toBeInTheDocument(); + }); +}); diff --git a/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.tsx b/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.tsx new file mode 100644 index 0000000000..eac0c4fa30 --- /dev/null +++ b/web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount.tsx @@ -0,0 +1,20 @@ +import { CURRENCIES } from '../DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +export function SupCurrencyAndAmount({ + price, + currencyCode, + className = '', +}: { + price: number; + currencyCode: string; + className?: string; +}) { + return currencyCode === CURRENCIES.usd ? ( + <> + $ + {price.toFixed(2)} + + ) : ( + {price} + ); +} diff --git a/web-components/src/components/buttons/EditButtonIcon.tsx b/web-components/src/components/buttons/EditButtonIcon.tsx new file mode 100644 index 0000000000..058650371d --- /dev/null +++ b/web-components/src/components/buttons/EditButtonIcon.tsx @@ -0,0 +1,29 @@ +import { cn } from 'web-components/src/utils/styles/cn'; + +import EditIcon from '../icons/EditIcon'; + +interface ButtonProps { + onClick: () => void; + className?: string; + ariaLabel?: string; +} + +export function EditButtonIcon({ + onClick, + className = '', + ariaLabel = '', +}: ButtonProps) { + return ( + + ); +} diff --git a/web-components/src/components/buttons/SetMaxButton.test.tsx b/web-components/src/components/buttons/SetMaxButton.test.tsx new file mode 100644 index 0000000000..4dc1213232 --- /dev/null +++ b/web-components/src/components/buttons/SetMaxButton.test.tsx @@ -0,0 +1,13 @@ +import { render } from '@testing-library/react'; +import { SetMaxButton } from 'web-components/src/components/buttons/SetMaxButton'; +import { fireEvent, screen } from 'web-components/test/test-utils'; + +describe('SetMaxButton', () => { + it('calls onClick when clicked', () => { + const onClick = vi.fn(); + render(); + const button = screen.getByLabelText('Set max credits'); + fireEvent.click(button); + expect(onClick).toHaveBeenCalledTimes(1); + }); +}); diff --git a/web-components/src/components/buttons/SetMaxButton.tsx b/web-components/src/components/buttons/SetMaxButton.tsx new file mode 100644 index 0000000000..381256ac90 --- /dev/null +++ b/web-components/src/components/buttons/SetMaxButton.tsx @@ -0,0 +1,18 @@ +import { MouseEvent } from 'react'; + +export function SetMaxButton({ + onClick, +}: { + onClick: (e: MouseEvent) => void; +}) { + return ( + + ); +} diff --git a/web-components/src/components/buttons/button.stories.tsx b/web-components/src/components/buttons/button.stories.tsx index b1cfb0e943..52ccfd71f3 100644 --- a/web-components/src/components/buttons/button.stories.tsx +++ b/web-components/src/components/buttons/button.stories.tsx @@ -5,10 +5,12 @@ import { Flex } from '../box'; import ContainedButton from './ContainedButton'; import { CopyButton } from './CopyButton'; import { EditButton } from './EditButton'; +import { EditButtonIcon } from './EditButtonIcon'; import { ExpandButton } from './ExpandButton'; import OutlinedButton from './OutlinedButton'; import PrevNextButton from './PrevNextButton'; import { SaveButton } from './SaveButton'; +import { SetMaxButton } from './SetMaxButton'; import { TableActionButtons } from './TableActionButtons'; import { TextButton } from './TextButton'; @@ -88,6 +90,8 @@ export const editButton = () => ( {}} /> ); +export const editButtonIcon = () => {}} />; + export const copyButton = { render: () => ( @@ -102,3 +106,11 @@ export const saveButton = { ), }; + +export const setMaxButton = { + render: () => ( + <> + {}} /> + + ), +}; diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Content.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Content.tsx new file mode 100644 index 0000000000..ced84c7c11 --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Content.tsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; +import { EditButtonIcon } from 'web-components/src/components/buttons/EditButtonIcon'; +import { DenomIconWithCurrency } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency'; +import { CURRENCIES } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import { EditableInput } from 'web-components/src/components/inputs/new/EditableInput/EditableInput'; +import { SupCurrencyAndAmount } from 'web-components/src/components/SupCurrencyAndAmount/SupCurrencyAndAmount'; +import { Title } from 'web-components/src/components/typography'; + +import { CRYPTO_TOOLTIP_TEXT } from './OrderSummaryCard.constants'; +import { OrderProps, PaymentMethod } from './OrderSummaryCard.types'; +import { OrderSummmaryRowHeader } from './OrderSummmaryCard.RowHeader'; + +export function OrderSummaryContent({ + order, + currentBuyingStep, + paymentMethod, + onClickEditCard = () => {}, +}: { + order: OrderProps; + currentBuyingStep: number; + paymentMethod: PaymentMethod; + onClickEditCard?: () => void; +}) { + const { projectName, currency, pricePerCredit, credits } = order; + const [creditsAmount, setCreditsAmount] = useState(credits); + return ( +
+ + Order Summary + + +

+ {projectName} +

+ +
+ + + + +
+ +
+ +
+
+
+
+
+ +
+ + + + +
+
+ {currentBuyingStep > 1 && + paymentMethod.type !== 'crypto' && + paymentMethod.cardNumber && ( +
+ +
+

+ + {paymentMethod.type} ending in + {' '} + {paymentMethod.cardNumber.slice(-4)} +

+ +
+
+ )} +
+ ); +} diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Image.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Image.tsx new file mode 100644 index 0000000000..11c503653e --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.Image.tsx @@ -0,0 +1,20 @@ +import { PrefinanceTag } from 'web-components/src/components/PrefinanceTag/PrefinanceTag'; + +export function OrderSummaryImage({ + src, + prefinanceProject, +}: { + src: string; + prefinanceProject?: boolean; +}) { + return ( +
+ {prefinanceProject && } + order summary +
+ ); +} diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.constants.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.constants.tsx new file mode 100644 index 0000000000..62952efe36 --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.constants.tsx @@ -0,0 +1,2 @@ +export const CRYPTO_TOOLTIP_TEXT = + 'Different sellers may sell the same credits at different prices. We automatically choose the lowest priced credits for you. This price is the average price of all the credits in your cart.'; diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.stories.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.stories.tsx new file mode 100644 index 0000000000..60160247fa --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.stories.tsx @@ -0,0 +1,81 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { CURRENCIES } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +import { OrderSummaryCard } from './OrderSummaryCard'; + +export default { + title: 'Cards/OrderSummaryCard', + component: OrderSummaryCard, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: args => , +}; + +Default.args = { + order: { + image: '/coorong.png', + projectName: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + prefinanceProject: false, + pricePerCredit: 2, + credits: 50, + currency: CURRENCIES.usd, + }, +}; + +export const WithPaymentDetails: Story = { + render: args => , +}; + +WithPaymentDetails.args = { + order: { + image: '/coorong.png', + projectName: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + prefinanceProject: false, + pricePerCredit: 2, + credits: 50, + currency: CURRENCIES.usd, + }, + currentBuyingStep: 2, + paymentMethod: { + type: 'visa', + cardNumber: '1234 5678 9012 3456', + }, +}; + +export const WithPrefinanceProject: Story = { + render: args => , +}; + +WithPrefinanceProject.args = { + order: { + image: '/coorong.png', + projectName: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + prefinanceProject: true, + pricePerCredit: 2, + credits: 50, + currency: 'usd', + }, + currentBuyingStep: 2, + paymentMethod: { + type: 'visa', + cardNumber: '1234 5678 9012 3456', + }, +}; + +export const WithCrypto: Story = { + render: args => , +}; + +WithCrypto.args = { + order: { + image: '/coorong.png', + projectName: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', + prefinanceProject: false, + pricePerCredit: 2, + credits: 50, + currency: CURRENCIES.uregen, + }, +}; diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.test.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.test.tsx new file mode 100644 index 0000000000..72e483034b --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.test.tsx @@ -0,0 +1,80 @@ +import { screen } from '@testing-library/dom'; +import { render } from '@testing-library/react'; +import { CURRENCIES } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import { fireEvent } from 'web-components/test/test-utils'; + +import { OrderSummaryCard } from './OrderSummaryCard'; +import { OrderSummaryProps } from './OrderSummaryCard.types'; + +describe('OrderSummaryCard', () => { + const orderSummary: OrderSummaryProps = { + order: { + projectName: 'Project Name', + currency: CURRENCIES.usd, + pricePerCredit: 10, + credits: 5, + image: 'path/to/image', + prefinanceProject: false, + }, + paymentMethod: { + type: 'visa', + cardNumber: '1234 5678 9012 3456', + }, + currentBuyingStep: 2, + onClickEditCard: vi.fn(), + }; + + it('displays the project name', () => { + render(); + const projectName = screen.getByText('Project Name'); + expect(projectName).toBeInTheDocument(); + }); + + it('displays the price per credit', () => { + render(); + const pricePerCredit = screen.getByText(/10.00/i); + expect(pricePerCredit).toBeInTheDocument(); + }); + + it('displays the number of credits', () => { + render(); + + const numberOfCredits = screen.getByText('5'); + expect(numberOfCredits).toBeInTheDocument(); + }); + + it('updates the number of credits and total price accordingly', () => { + render(); + + const editButton = screen.getByRole('button', { + name: 'Edit', + }); + fireEvent.click(editButton); + const editInput = screen.getByRole('textbox', { + name: 'editable-credits', + }); + fireEvent.change(editInput, { target: { value: 7 } }); + + const updateButton = screen.getByRole('button', { + name: 'update', + }); + fireEvent.click(updateButton); + const updatedNumberOfCredits = screen.getByText('7'); + expect(updatedNumberOfCredits).toBeInTheDocument(); + + const pricePerCredit = screen.getByText(/70.00/i); + expect(pricePerCredit).toBeInTheDocument(); + }); + + it('displays the total price', () => { + render(); + const totalPrice = screen.getByText(/50.00/i); + expect(totalPrice).toBeInTheDocument(); + }); + + it('displays the payment details', () => { + render(); + const payment = screen.getByTestId('payment-details'); + expect(payment.textContent).toMatch(/visa ending in 3456/i); + }); +}); diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.tsx new file mode 100644 index 0000000000..9555a25fb1 --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.tsx @@ -0,0 +1,23 @@ +import Card from '../Card'; +import { OrderSummaryContent } from './OrderSummaryCard.Content'; +import { OrderSummaryImage } from './OrderSummaryCard.Image'; +import { OrderSummaryProps } from './OrderSummaryCard.types'; + +export const OrderSummaryCard = (orderSummary: OrderSummaryProps) => { + const { order, paymentMethod, currentBuyingStep, onClickEditCard } = + orderSummary; + return ( + + + + + ); +}; diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.types.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.types.tsx new file mode 100644 index 0000000000..5de68d4a31 --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummaryCard.types.tsx @@ -0,0 +1,28 @@ +import { Currency } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +export interface OrderProps { + projectName: string; + prefinanceProject: boolean; + pricePerCredit: number; + credits: number; + currency: Currency; + image: string; +} + +export interface OrderSummaryProps { + order: OrderProps; + // TO-DO remove currentBuyingStep prop and get the current step from the context + // this cound be a number or a string (choose credits | payment info | retirement | complete |) + currentBuyingStep: number; + // TO-DO: get from the context + paymentMethod: { + type: 'visa' | 'mastercard'; + cardNumber: string; + }; + onClickEditCard: () => void; +} + +export interface PaymentMethod { + type: 'visa' | 'mastercard' | 'crypto'; + cardNumber: string; +} diff --git a/web-components/src/components/cards/OrderSummaryCard/OrderSummmaryCard.RowHeader.tsx b/web-components/src/components/cards/OrderSummaryCard/OrderSummmaryCard.RowHeader.tsx new file mode 100644 index 0000000000..0865bbc2c1 --- /dev/null +++ b/web-components/src/components/cards/OrderSummaryCard/OrderSummmaryCard.RowHeader.tsx @@ -0,0 +1,18 @@ +import { Title } from 'web-components/src/components/typography'; + +export function OrderSummmaryRowHeader({ + text, + className = '', +}: { + text: string; + className?: string; +}) { + return ( + + {text} + + ); +} diff --git a/web-components/src/components/cards/ProjectCard/ProjectCard.tsx b/web-components/src/components/cards/ProjectCard/ProjectCard.tsx index 7724072d84..f071440a46 100644 --- a/web-components/src/components/cards/ProjectCard/ProjectCard.tsx +++ b/web-components/src/components/cards/ProjectCard/ProjectCard.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { SxProps, Theme, useTheme } from '@mui/material'; import clsx from 'clsx'; import InfoTooltip from 'web-components/src/components/tooltip/InfoTooltip'; +import { PrefinanceTag } from 'web-components/src/components/PrefinanceTag/PrefinanceTag'; import { Track } from 'web-marketplace/src/lib/tracker/types'; import { ButtonType } from '../../../types/shared/buttonType'; @@ -10,16 +11,14 @@ import { cn } from '../../../utils/styles/cn'; import { BlockContent, SanityBlockContent } from '../../block-content'; import ContainedButton from '../../buttons/ContainedButton'; import BreadcrumbIcon from '../../icons/BreadcrumbIcon'; -import { PrefinanceIcon } from '../../icons/PrefinanceIcon'; import ProjectPlaceInfo from '../../place/ProjectPlaceInfo'; -import { Body, Label } from '../../typography'; +import { Body } from '../../typography'; import { Account, User } from '../../user/UserInfo'; import MediaCard, { MediaCardProps } from '../MediaCard/MediaCard'; import { ProjectCardButton } from './ProjectCard.Button'; import { AVG_PRICE_TOOLTIP, DEFAULT_BUY_BUTTON, - PREFINANCE, PREFINANCE_BUTTON, PREFINANCE_PRICE_TOOLTIP, VIEW_PROJECT_BUTTON, @@ -173,16 +172,12 @@ export function ProjectCard({ /> {isPrefinanceProject && ( -
- - -
+ )} {comingSoon && (
diff --git a/web-components/src/components/cards/card.stories.tsx b/web-components/src/components/cards/card.stories.tsx index b7b0ef82d8..969f7bae9a 100644 --- a/web-components/src/components/cards/card.stories.tsx +++ b/web-components/src/components/cards/card.stories.tsx @@ -118,10 +118,29 @@ export const projectCard = (): JSX.Element => ( imgSrc="/coorong.png" tag="biodiversity" onClick={onClick} - sx={{ maxWidth: 338 }} + sx={{ maxWidth: 338, mr: 10, mb: 10 }} draft draftText="Draft" /> + + ); diff --git a/web-components/src/components/icons/CreditCardIcon.tsx b/web-components/src/components/icons/CreditCardIcon.tsx new file mode 100644 index 0000000000..5ddd192b14 --- /dev/null +++ b/web-components/src/components/icons/CreditCardIcon.tsx @@ -0,0 +1,28 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +interface IconProps extends SvgIconProps {} + +export default function CreditCardIcon({ + sx = [], + ...props +}: IconProps): JSX.Element { + return ( + + + + + + ); +} diff --git a/web-components/src/components/icons/CryptoIcon.tsx b/web-components/src/components/icons/CryptoIcon.tsx new file mode 100644 index 0000000000..26032f4339 --- /dev/null +++ b/web-components/src/components/icons/CryptoIcon.tsx @@ -0,0 +1,144 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +interface IconProps extends SvgIconProps {} + +export default function CryptoIcon({ + sx = [], + ...props +}: IconProps): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +{ + /* + + + + + + + + + + + + + + + + + + + + + + + + + + + + */ +} diff --git a/web-components/src/components/icons/LeafIcon.tsx b/web-components/src/components/icons/LeafIcon.tsx new file mode 100644 index 0000000000..713b8091f7 --- /dev/null +++ b/web-components/src/components/icons/LeafIcon.tsx @@ -0,0 +1,139 @@ +interface IconProps { + className?: string; +} + +export const LeafIcon = ({ className }: IconProps): JSX.Element => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/web-components/src/components/icons/flags/USFlagIcon.tsx b/web-components/src/components/icons/flags/USFlagIcon.tsx new file mode 100644 index 0000000000..3c0dcc0e9d --- /dev/null +++ b/web-components/src/components/icons/flags/USFlagIcon.tsx @@ -0,0 +1,99 @@ +import SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon'; + +interface IconProps extends SvgIconProps {} + +export default function USFlagIcon({ + sx = [], + ...props +}: IconProps): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/web-components/src/components/icons/icons.stories.tsx b/web-components/src/components/icons/icons.stories.tsx index 91b6f7211f..9dd7c0408c 100644 --- a/web-components/src/components/icons/icons.stories.tsx +++ b/web-components/src/components/icons/icons.stories.tsx @@ -31,11 +31,13 @@ import { CopyIcon } from './CopyIcon'; import CountingIcon from './CountingIcon'; import { CreditBatchIcon } from './CreditBatchIcon'; import { CreditBatchLightIcon } from './CreditBatchLightIcon'; +import CreditCardIcon from './CreditCardIcon'; import { CreditClassIcon } from './CreditClassIcon'; import CreditsIcon from './CreditsIcon'; import CreditsIssuedIcon from './CreditsIssued'; import CreditsRetiredIcon from './CreditsRetired'; import CreditsTradeableIcon from './CreditsTradeable'; +import CryptoIcon from './CryptoIcon'; import CurrentCreditsIcon from './CurrentCreditsIcon'; import DocumentIcon from './DocumentIcon'; import { DraftDocumentIcon } from './DraftDocumentIcon'; @@ -48,6 +50,7 @@ import ErrorIcon from './ErrorIcon'; import EyeIcon from './EyeIcon'; import FarmerIcon from './FarmerIcon'; import FilterIcon from './FilterIcon'; +import USFlagIcon from './flags/USFlagIcon'; import { GreenPinIcon } from './GreenPinIcon'; import { HorizontalDotsIcon } from './HorizontalDotsIcon'; import { ImageIcon } from './ImageIcon'; @@ -384,5 +387,8 @@ export const allIcons = (): JSX.Element => ( } label="YoutubeIcon" /> } label="ZoomIcon" /> } label="WarningIcon" /> + } label="USFlagIcon" /> + } label="CreditCardIcon" /> + } label="CryptoIcon" /> ); diff --git a/web-components/src/components/inputs/new/CustomSelect/CustomSelect.Option.tsx b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.Option.tsx new file mode 100644 index 0000000000..c1ac470463 --- /dev/null +++ b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.Option.tsx @@ -0,0 +1,28 @@ +import { CryptoCurrencies } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import { Option } from 'web-components/src/components/inputs/new/CustomSelect/CustomSelect.types'; + +type SelectOptionProps = { + option: Option; + handleSelect: (currency: CryptoCurrencies | string) => void; +}; + +export const SelectOption = ({ option, handleSelect }: SelectOptionProps) => { + const handleClick = () => { + if (option?.value && 'value' in option) { + handleSelect(option.value); + } else if (option?.component?.label) { + handleSelect(option.component.label as CryptoCurrencies); + } + }; + + return ( + + ); +}; diff --git a/web-components/src/components/inputs/new/CustomSelect/CustomSelect.Placeholder.tsx b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.Placeholder.tsx new file mode 100644 index 0000000000..921992b0fa --- /dev/null +++ b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.Placeholder.tsx @@ -0,0 +1,33 @@ +import { ComponentType } from 'react'; +import BreadcrumbIcon from 'web-components/src/components/icons/BreadcrumbIcon'; +import { Option } from 'web-components/src/components/inputs/new/CustomSelect/CustomSelect.types'; + +export function Placeholder({ + setIsOpen, + isOpen, + options, + selectedOption, + OptionComponent, +}: { + setIsOpen: (isOpen: boolean) => void; + isOpen: boolean; + options: Option[]; + selectedOption: string; + OptionComponent: ComponentType; +}) { + return ( + + ); +} diff --git a/web-components/src/components/inputs/new/CustomSelect/CustomSelect.stories.tsx b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.stories.tsx new file mode 100644 index 0000000000..84d3638807 --- /dev/null +++ b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.stories.tsx @@ -0,0 +1,46 @@ +import { lazy } from 'react'; +import { Meta, StoryObj } from '@storybook/react'; + +const CustomSelect = lazy(() => import('./CustomSelect')); + +export default { + title: 'Inputs/CustomSelect', + component: CustomSelect, +} as Meta; + +type Story = StoryObj; + +const options = [ + { label: 'USD', value: 'usd' }, + { label: 'UREGEN', value: 'uregen' }, + { label: 'USDC', value: 'usdc' }, +]; + +const Component1 = () =>
🌲 Component 1
; +const Component2 = () =>
🌴 Component 2
; +const Component3 = () =>
🌻 Component 3
; +const componentOptions = [ + { component: { label: 'Component1', element: Component1 } }, + { component: { label: 'Component2', element: Component2 } }, + { component: { label: 'Component3', element: Component3 } }, +]; + +export const WithText: Story = { + render: args => , +}; + +WithText.args = { + options: options, + onSelect: () => {}, + defaultOption: 'usd', +}; + +export const WithComponent: Story = { + render: args => , +}; + +WithComponent.args = { + options: componentOptions, + onSelect: () => {}, + defaultOption: 'Component1', +}; diff --git a/web-components/src/components/inputs/new/CustomSelect/CustomSelect.test.tsx b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.test.tsx new file mode 100644 index 0000000000..3984cd9a4d --- /dev/null +++ b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.test.tsx @@ -0,0 +1,119 @@ +import { render } from '@testing-library/react'; +import { fireEvent, screen } from 'web-components/test/test-utils'; + +import CustomSelect from './CustomSelect'; + +describe('CustomSelect', () => { + const defaultOption = 'usd'; + const onSelect = vi.fn(); + const options = [ + { label: 'USD', value: 'usd' }, + { label: 'UREGEN', value: 'uregen' }, + { label: 'USDC', value: 'usdc' }, + ]; + + const Component1 = () =>
Component1
; + const Component2 = () =>
Component2
; + const componentOption = [ + { component: { label: 'Component1', element: Component1 } }, + { component: { label: 'Component2', element: Component2 } }, + ]; + + it('renders the default option', () => { + render( + , + ); + + const defaultOptionElement = screen.getByText(/usd/i); + expect(defaultOptionElement).toBeInTheDocument(); + }); + + it('opens the dropdown when clicked', () => { + render( + , + ); + + const dropdownButton = screen.getByRole('button'); + fireEvent.click(dropdownButton); + + const dropdownMenu = screen.getByRole('menu'); + expect(dropdownMenu).toBeInTheDocument(); + }); + + it('calls onSelect when an option is selected', () => { + render( + , + ); + + const dropdownButton = screen.getByRole('button'); + fireEvent.click(dropdownButton); + + const optionButton = screen.getByText(/uregen/i); + fireEvent.click(optionButton); + + expect(onSelect).toHaveBeenCalledWith('uregen'); + }); + + it('renders a component option', async () => { + render( + , + ); + + const dropdownButton = screen.getByLabelText('Select options'); + fireEvent.click(dropdownButton); + const optionButton = screen.getAllByLabelText('Select option')[0]; + expect(optionButton).toBeInTheDocument(); + }); + + it('renders the selected option in the placeholder', () => { + render( + , + ); + + const dropdownButton = screen.getByLabelText('Select options'); + fireEvent.click(dropdownButton); + const optionButton = screen.getAllByLabelText('Select option')[0]; + fireEvent.click(optionButton); + + const selectedOption = screen.getByText(/Component1/i); + expect(selectedOption).toBeInTheDocument(); + }); + + it('closes the dropdown when an option is selected', () => { + render( + , + ); + + const dropdownButton = screen.getByLabelText('Select options'); + fireEvent.click(dropdownButton); + const optionButton = screen.getAllByLabelText('Select option')[0]; + fireEvent.click(optionButton); + + const dropdownMenu = screen.queryByRole('menu'); + expect(dropdownMenu).not.toBeInTheDocument(); + }); +}); diff --git a/web-components/src/components/inputs/new/CustomSelect/CustomSelect.tsx b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.tsx new file mode 100644 index 0000000000..e47bcd234e --- /dev/null +++ b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.tsx @@ -0,0 +1,76 @@ +import { ComponentType, useEffect, useState } from 'react'; +import { CryptoCurrencies } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import { Option } from 'web-components/src/components/inputs/new/CustomSelect/CustomSelect.types'; + +import { SelectOption } from './CustomSelect.Option'; +import { Placeholder } from './CustomSelect.Placeholder'; + +const CustomSelect = ({ + options, + onSelect, + defaultOption, +}: { + options: Option[]; + onSelect: (currency: CryptoCurrencies | string) => void; + defaultOption: string; +}) => { + const [isOpen, setIsOpen] = useState(false); + const [selectedOption, setSelectedOption] = useState(defaultOption); + const [OptionComponent, setOptionComponent] = useState( + () => options[0].component?.element as ComponentType, + ); + + const handleSelect = (option: CryptoCurrencies | string) => { + setSelectedOption(option); + onSelect(option); + setIsOpen(false); + }; + + useEffect(() => { + const option = + options.find(opt => opt.value === selectedOption) || + options.find(opt => opt.component?.label === selectedOption); + if (option && 'component' in option) { + const NextOption = option?.component?.element as ComponentType; + setOptionComponent(() => NextOption); + } + }, [options, selectedOption, setOptionComponent]); + + return ( +
+ + {isOpen && ( +
+ {options + .filter( + option => + option.component?.label !== selectedOption && + option.label !== selectedOption, + ) + .map((option, i) => { + return ( + + ); + })} +
+ )} +
+ ); +}; + +export default CustomSelect; diff --git a/web-components/src/components/inputs/new/CustomSelect/CustomSelect.types.tsx b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.types.tsx new file mode 100644 index 0000000000..0afa7de4b3 --- /dev/null +++ b/web-components/src/components/inputs/new/CustomSelect/CustomSelect.types.tsx @@ -0,0 +1,18 @@ +import { ComponentType } from 'react'; + +export type OptionWithLabelAndValue = { + label: string; + value: string; + component?: never; +}; + +export type OptionWithComponent = { + label?: never; + value?: never; + component: { + label: string; + element: ComponentType; + }; +}; + +export type Option = OptionWithLabelAndValue | OptionWithComponent; diff --git a/web-components/src/components/inputs/new/EditableInput/EditableInput.stories.tsx b/web-components/src/components/inputs/new/EditableInput/EditableInput.stories.tsx new file mode 100644 index 0000000000..af4dddaccf --- /dev/null +++ b/web-components/src/components/inputs/new/EditableInput/EditableInput.stories.tsx @@ -0,0 +1,19 @@ +import { Meta, StoryObj } from '@storybook/react'; + +import { EditableInput } from './EditableInput'; + +export default { + title: 'Inputs/EditableInput', + component: EditableInput, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = { + render: args => , +}; + +Default.args = { + value: 5, + onChange: () => {}, +}; diff --git a/web-components/src/components/inputs/new/EditableInput/EditableInput.test.tsx b/web-components/src/components/inputs/new/EditableInput/EditableInput.test.tsx new file mode 100644 index 0000000000..bde3506d9a --- /dev/null +++ b/web-components/src/components/inputs/new/EditableInput/EditableInput.test.tsx @@ -0,0 +1,76 @@ +import { render } from '@testing-library/react'; +import { fireEvent, screen } from 'web-components/test/test-utils'; + +import { EditableInput } from './EditableInput'; + +describe('EditableInput', () => { + it('renders the amount and edit button', () => { + const onChangeMock = vi.fn(); + render( + , + ); + const amount = screen.getByText('100'); + expect(amount).toBeInTheDocument(); + const editButton = screen.getByRole('button', { + name: 'Edit', + }); + + expect(editButton).toBeInTheDocument(); + }); + + it('renders the input field and update button when when click edit', () => { + const onChangeMock = vi.fn(); + render( + , + ); + const editButton = screen.getByRole('button', { + name: 'Edit', + }); + fireEvent.click(editButton); + const input = screen.getByRole('textbox', { + name: 'testEditableInput', + }); + expect(input).toBeInTheDocument(); + + const updateButton = screen.getByRole('button', { + name: 'update', + }); + expect(updateButton).toBeInTheDocument(); + }); + + it('calls the onChange callback with the updated amount when click update', () => { + const onChangeMock = vi.fn(); + render( + , + ); + + const editButton = screen.getByRole('button', { + name: 'Edit', + }); + fireEvent.click(editButton); + + const input = screen.getByRole('textbox', { + name: 'testEditableInput', + }); + fireEvent.change(input, { target: { value: '200' } }); + + const updateButton = screen.getByRole('button', { + name: 'update', + }); + fireEvent.click(updateButton); + + expect(onChangeMock).toHaveBeenCalledWith(200); + }); +}); diff --git a/web-components/src/components/inputs/new/EditableInput/EditableInput.tsx b/web-components/src/components/inputs/new/EditableInput/EditableInput.tsx new file mode 100644 index 0000000000..23b30a0d1c --- /dev/null +++ b/web-components/src/components/inputs/new/EditableInput/EditableInput.tsx @@ -0,0 +1,80 @@ +import { ChangeEvent, KeyboardEvent, useState } from 'react'; +import { EditButtonIcon } from 'web-components/src/components/buttons/EditButtonIcon'; +import { TextButton } from 'web-components/src/components/buttons/TextButton'; + +interface EditableInputProps { + value: number; + onChange: (amount: number) => void; + name?: string; + ariaLabel?: string; + className?: string; +} + +export const EditableInput = ({ + value, + onChange, + name = '', + ariaLabel = 'Editable text input', + className = '', +}: EditableInputProps) => { + const [editable, setEditable] = useState(false); + const [amount, setAmount] = useState(value); + + const toggleEditable = () => { + setEditable(!editable); + }; + + const handleOnChange = (e: ChangeEvent) => { + e.preventDefault(); + if (isNaN(+e.target.value)) return; + setAmount(+e.target.value); + }; + + const handleOnUpdate = () => { + onChange(+amount); + toggleEditable(); + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + if (isNaN(+e.currentTarget.value)) return; + setAmount(+e.currentTarget.value); + handleOnUpdate(); + } + }; + + return ( + <> + {editable ? ( +
+ + + update + +
+ ) : ( +
+ {amount} + +
+ )} + + ); +}; diff --git a/web-components/src/components/inputs/new/Radio/Radio.tsx b/web-components/src/components/inputs/new/Radio/Radio.tsx index 77df2d0777..977a68e1a4 100644 --- a/web-components/src/components/inputs/new/Radio/Radio.tsx +++ b/web-components/src/components/inputs/new/Radio/Radio.tsx @@ -1,4 +1,4 @@ -import { forwardRef, ReactNode } from 'react'; +import { ChangeEvent, forwardRef, ReactNode } from 'react'; import { Box, FormControlLabel, @@ -17,9 +17,9 @@ import { useRadioStyles } from './Radio.styles'; import { RadiotVariant } from './Radio.types'; export interface RadioProps extends RadioPropsMui { - value?: string; + value?: string | boolean; label?: ReactNode; - selectedValue?: string; + selectedValue?: string | boolean; variant?: RadiotVariant; optional?: string | boolean; helperText?: string | JSX.Element; @@ -27,6 +27,7 @@ export interface RadioProps extends RadioPropsMui { sx?: SxProps; description?: ReactNode; tooltip?: ReactNode; + onChange?: (e: ChangeEvent) => void; } export const Radio = forwardRef( @@ -43,6 +44,7 @@ export const Radio = forwardRef( description, tooltip, disabled, + onChange, ...props }, ref, @@ -89,6 +91,7 @@ export const Radio = forwardRef( icon={} sx={{ p: 0 }} ref={ref} + onChange={onChange} /> } sx={{ ml: 0, mr: 1 }} diff --git a/web-components/src/components/inputs/new/TextField/TextField.types.ts b/web-components/src/components/inputs/new/TextField/TextField.types.ts index fbf51e5ae8..f244119064 100644 --- a/web-components/src/components/inputs/new/TextField/TextField.types.ts +++ b/web-components/src/components/inputs/new/TextField/TextField.types.ts @@ -7,7 +7,12 @@ export interface RegenTextFieldProps extends StandardTextFieldProps { startAdornment?: ReactNode; endAdornment?: ReactNode; step?: number | string; - customInputProps?: { min?: number; max?: number; step?: string | number }; + customInputProps?: { + min?: number; + max?: number; + step?: string | number; + 'aria-label'?: string; + }; description?: string | ReactNode; label?: ReactNode; className?: string; diff --git a/web-components/src/components/molecules/InfoCard/InfoCard.stories.tsx b/web-components/src/components/molecules/InfoCard/InfoCard.stories.tsx index 37d477c0c3..b5830c5c37 100644 --- a/web-components/src/components/molecules/InfoCard/InfoCard.stories.tsx +++ b/web-components/src/components/molecules/InfoCard/InfoCard.stories.tsx @@ -19,3 +19,32 @@ Default.args = { }, description: 'Contact sales@regen.network or schedule a call.', }; + +export const WithComponents = Template.bind({}); +WithComponents.args = { + title: 'Looking for over-the-counter sales?', + image: { + src: '/illustrations/concierge-small.svg', + }, + description: ( +

+ Contact{' '} + + sales@regen.network + + or + + schedule a call + +

+ ), +}; diff --git a/web-components/src/components/molecules/InfoCard/InfoCard.tsx b/web-components/src/components/molecules/InfoCard/InfoCard.tsx index 31a16744e6..d7919c6edd 100644 --- a/web-components/src/components/molecules/InfoCard/InfoCard.tsx +++ b/web-components/src/components/molecules/InfoCard/InfoCard.tsx @@ -1,3 +1,4 @@ +import { isValidElement } from 'react'; import { Box, SxProps } from '@mui/material'; import { BlockContent } from '../../../components/block-content'; @@ -42,7 +43,11 @@ const InfoCard = ({ {parseText(title)} - + {isValidElement(description) ? ( + description + ) : ( + + )} diff --git a/web-components/src/components/sliders/ProjectMedia.PrefinanceTag.tsx b/web-components/src/components/sliders/ProjectMedia.PrefinanceTag.tsx deleted file mode 100644 index 74d24d36d3..0000000000 --- a/web-components/src/components/sliders/ProjectMedia.PrefinanceTag.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { PREFINANCE } from '../cards/ProjectCard/ProjectCard.constants'; -import { PrefinanceIcon } from '../icons/PrefinanceIcon'; -import { Label } from '../typography'; - -export const PrefinanceTag = () => ( -
- - -
-); diff --git a/web-components/src/components/sliders/ProjectMedia.tsx b/web-components/src/components/sliders/ProjectMedia.tsx index 5befd50eae..e6f825f2fa 100644 --- a/web-components/src/components/sliders/ProjectMedia.tsx +++ b/web-components/src/components/sliders/ProjectMedia.tsx @@ -8,12 +8,9 @@ import { makeStyles } from 'tss-react/mui'; import { containerPaddingX, containerStyles } from '../../styles/container'; import { getOptimizedImageSrc } from '../../utils/optimizedImageSrc'; -import { PREFINANCE } from '../cards/ProjectCard/ProjectCard.constants'; import PlayIcon from '../icons/PlayIcon'; -import { PrefinanceIcon } from '../icons/PrefinanceIcon'; import { Image, OptimizeImageProps } from '../image'; -import { Label } from '../typography'; -import { PrefinanceTag } from './ProjectMedia.PrefinanceTag'; +import { PrefinanceTag } from '../PrefinanceTag/PrefinanceTag'; import { ProjectAsset } from './ProjectMedia.ProjectAsset'; export interface Media { @@ -324,7 +321,15 @@ export default function ProjectMedia({ {i === 0 && isMedia(a) && imageCredits && ( {imageCredits} )} - {i === 0 && isPrefinanceProject && } + {i === 0 && isPrefinanceProject && ( + + )} ))} @@ -413,7 +418,15 @@ export default function ProjectMedia({ } })} - {isPrefinanceProject && } + {isPrefinanceProject && ( + + )}
)} diff --git a/web-components/src/components/tooltip/InfoTooltipWithIcon.tsx b/web-components/src/components/tooltip/InfoTooltipWithIcon.tsx index ca953e6e63..b38c529dcc 100644 --- a/web-components/src/components/tooltip/InfoTooltipWithIcon.tsx +++ b/web-components/src/components/tooltip/InfoTooltipWithIcon.tsx @@ -9,17 +9,25 @@ interface Props { title: TooltipProps['title']; outlined?: boolean; sx?: SxProps; + className?: string; + placement?: TooltipProps['placement']; } export default function InfoTooltipWithIcon({ title, outlined, sx, + className = '', + placement = 'top', }: Props): JSX.Element { return ( - - - {outlined ? : } + + + {outlined ? ( + + ) : ( + + )} ); diff --git a/web-components/src/components/tooltip/QuestionMarkTooltip.tsx b/web-components/src/components/tooltip/QuestionMarkTooltip.tsx index c55896d45a..ead456ed5d 100644 --- a/web-components/src/components/tooltip/QuestionMarkTooltip.tsx +++ b/web-components/src/components/tooltip/QuestionMarkTooltip.tsx @@ -1,4 +1,5 @@ import { Box, SxProps, TooltipProps } from '@mui/material'; +import { cn } from 'web-components/src/utils/styles/cn'; import { Theme } from '../../theme/muiTheme'; import { sxToArray } from '../../utils/mui/sxToArray'; @@ -11,6 +12,7 @@ interface Props { sx?: SxProps; color?: string; size?: TextSize; + placement?: TooltipProps['placement']; className?: string; } @@ -19,10 +21,17 @@ export default function QuestionMarkTooltip({ sx, color = 'secondary.main', size = 'sm', - className, + placement = 'top', + className = '', }: Props): JSX.Element { return ( - + ({ - matches: mediaQuery.match(query, { - width, - }), - addListener: () => {}, - removeListener: () => {}, - }); -} - -describe('App', () => { - beforeAll(() => { - // @ts-ignore - window.matchMedia = createMatchMedia(window.innerWidth); - }); - - describe('App', () => { - it('renders without crashing', () => { - const router = createMemoryRouter( - getRegenRoutes({ reactQueryClient, apolloClientFactory }), - ); - const container = document.createElement('div'); - document.body.appendChild(container); - ReactDOM.render( - - - - - - - , - , - container, - ); - unmountComponentAtNode(container); - }); - }); -}); diff --git a/web-marketplace/src/__snapshots__/storyshots.test.ts.snap b/web-marketplace/src/__snapshots__/storyshots.test.ts.snap deleted file mode 100644 index 8f01bb2a0c..0000000000 --- a/web-marketplace/src/__snapshots__/storyshots.test.ts.snap +++ /dev/null @@ -1,5237 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots Action Action 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Action Action Long Description 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Blog Post Blog Post 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Combined 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Contained Button 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Edit Button 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Expand Button 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Next Button 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Outlined Button 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Prev Button 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Table Action Buttons 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Buttons Text Button 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Glance Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Green Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Green Top Icon Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Image Action Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Impact Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Map Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Monitored Impact Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards On Boarding Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Overview Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Overview Cards Group 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Project Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Project Impact Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Project Top Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Purchased Credits Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Resources Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Review Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards Step Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/Create Card Create Credit Class Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/Create Card Create Project Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/Create Card First Credit Class Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/Create Card First Project Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/Create Card No Title Or Icon 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/Create Card With Title And Icon 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/CreditClassCard Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/ReviewCard Item Display 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/ReviewCard Photo 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/ReviewCard Review Card 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Cards/ReviewCard Review Card Base 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Certificate Certificate 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Charts Bar Chart 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Credits Credit Details 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Credits Credits Gauge 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Document Document 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots EmptyState Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots EmptyState With Children 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots FAQ Category 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots FAQ Faq 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots FAQ Navigation 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots FAQ Question 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Footers Fixed Footer 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Footers Save Footer 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Footers Switch Footer 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Forms Basket Put Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Forms Basket Take Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Forms Credit Batch Recipients Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Forms Login Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Forms Sign Up Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Forms User Profile 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots GettingStartedResourcesCard Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots GradientBadge Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Header Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Icons All Icons 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Image Grid Image Grid 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Image Image Default Quality 100 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Image Item Image Item 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Image Original Image 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots InfoLabel Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Inputs Date Pick Field 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Inputs Select Text Field 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Inputs Toggle 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Loading Loading Spinner 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Map Map 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots MarketplaceLaunchBanner Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Basket Put Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Basket Take Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Confirm Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Create Sell Order Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Crop Round Image Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Crop Square Image Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Ledger Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Processing Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Tx Error Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal Tx Successful Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal/AddWalletModalConnect Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal/AddWalletModalRemove Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal/AddWalletModalSwitch Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Modal/WalletModal Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Pagination Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Account Account Address 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Place Project Place Info 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots ReadMore Read More 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Molecules/Role Field Roles Input 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Molecules/Scrollable Codebox Scrollable Codebox 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Bridge Modal Bridge Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Class Form Create Credit Class Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Class Form Credit Class Finished 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Class Form Credit Class Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Class Form Credit Class Review 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Retire Modal Credit Send Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Send Form Credit Retire Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Send Form Credit Send Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Credit Send Modal Credit Send Modal 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/EditProfileForm Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/MediaForm Media Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Organisms/Project Metadata Form Project Metadata Form 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Registry/Templates/Multi Step Template Multi Step Template 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots ShareSection Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Sliders Project Media 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Sliders Project Media With Map 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Sliders Protected Species 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots StickyBar Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Table Actions Table 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Table Documentation Table 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Table Table Pagination 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Tabs Icon Tabs 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Tabs Mrv Tabs 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Text Layouts Labeled Detail 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Text Layouts Labeled Number 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Text Layouts Title Description 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Timeline New Timeline 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Timeline Timeline 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Timeline Timeline Item 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Tooltip Info 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Tooltip Info With Icon 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Tooltip Main Title 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Typography Body 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Typography Label 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Typography Subtitle 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Typography Title 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots User Big User Avatar 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots User Column User Info 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots User Fallback User Avatar 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots User Medium User Avatar 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots User User Avatar With Link 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots User User Info 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots User With Title 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Views Error View 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots Views Not Found View 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots atoms/CardRibbon Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots atoms/RadioCard Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots molecules/ActionCard Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots molecules/ActionCard Default 2 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots molecules/EcologicalCreditCard Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots molecules/InfoCard Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots molecules/StatCard Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots organisms/CancelButtonFooter Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots organisms/CardsGridContainer Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots organisms/CarouselSection Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots organisms/Gallery Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots organisms/Section Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; - -exports[`Storyshots organisms/StatCardsSection Default 1`] = ` - - - - - - - - - } - path="/" - > - - - - -`; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx new file mode 100644 index 0000000000..34aa50d64a --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx @@ -0,0 +1,69 @@ +import { useFormContext } from 'react-hook-form'; +import { Trans } from '@lingui/macro'; +import { PAYMENT_OPTIONS } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants'; +import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; + +import { SetMaxButton } from 'web-components/src/components/buttons/SetMaxButton'; +import { DenomIconWithCurrency } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency'; +import { + CURRENCIES, + Currency, +} from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import { Title } from 'web-components/src/components/typography/Title'; + +export function CreditsAmountHeader({ + creditsAvailable, + setMaxCreditsSelected, + currency, + paymentOption, +}: { + creditsAvailable: number; + setMaxCreditsSelected: (value: boolean) => void; + currency: Currency; + paymentOption: string; +}) { + const cryptoCurrency = + currency === CURRENCIES.usd ? CURRENCIES.uregen : currency; + const { clearErrors } = useFormContext(); + return ( +
+ + <Trans>Amount</Trans> + +
+
+ + + {creditsAvailable} + + credits available + + {paymentOption === PAYMENT_OPTIONS.CRYPTO && ( + + + in + + + + )} +
+ { + event.preventDefault(); + setMaxCreditsSelected(true); + clearErrors(); + }} + /> +
+
+ ); +} diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts new file mode 100644 index 0000000000..7ea35a7d4b --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants.ts @@ -0,0 +1,26 @@ +import { msg } from '@lingui/macro'; + +import { CURRENCIES } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +export const CREDITS_AMOUNT = 'creditsAmount'; +export const CURRENCY_AMOUNT = 'currencyAmount'; +export const CREDIT_VINTAGE_OPTIONS = 'creditVintageOptions'; +export const RETIRING = 'retiring'; +export const DEFAULT_CRYPTO_CURRENCY = CURRENCIES.uregen; + +export const cryptoOptions = [ + { + label: msg`Retire credits now`, + description: msg`These credits will be retired upon purchase and will not be tradeable. Retirement is permanent and non-reversible.`, + linkTo: + 'https://guides.regen.network/guides/regen-marketplace-buyers-guides/ecocredits/retire-ecocredits/retirement-certification#individual-entity-credit-retirement.', + value: true, + }, + { + label: msg`Buy tradable ecocredits`, + description: msg`These credits will be a tradeable asset. They can be retired later via Regen Marketplace.`, + linkTo: + 'https://guides.regen.network/guides/regen-marketplace-buyers-guides/ecocredits/retire-ecocredits/retirement-certification#individual-entity-credit-retirement.', + value: false, + }, +]; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.mock.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.mock.tsx new file mode 100644 index 0000000000..a87b835de3 --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.mock.tsx @@ -0,0 +1,42 @@ +import { CURRENCIES } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +export const creditVintages = [ + { + date: 'Jan 1, 2019 - December 31, 2019', + credits: '100', + batchDenom: 'mock-batch-denom-1', + }, + { + date: 'Jan 1, 2020 - December 31, 2020', + credits: '200', + batchDenom: 'mock-batch-denom-2', + }, + { + date: 'Jan 1, 2021 - December 31, 2021', + credits: '300', + batchDenom: 'mock-batch-denom-3', + }, +]; + +export const creditDetails = [ + { + availableCredits: 1000, + currency: CURRENCIES.usd, + creditPrice: 1, + }, + { + availableCredits: 2000, + currency: CURRENCIES.uregen, + creditPrice: 0.5, + }, + { + availableCredits: 3000, + currency: CURRENCIES.usdc, + creditPrice: 2, + }, + { + availableCredits: 4000, + currency: CURRENCIES.usdcaxl, + creditPrice: 3, + }, +]; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx new file mode 100644 index 0000000000..f6a2ceaa11 --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.stories.tsx @@ -0,0 +1,70 @@ +import { Meta, StoryObj } from '@storybook/react'; +import Form from 'web-marketplace/src/components/molecules/Form/Form'; +import { useZodForm } from 'web-marketplace/src/components/molecules/Form/hook/useZodForm'; +import { PAYMENT_OPTIONS } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants'; +import { createChooseCreditsFormSchema } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; + +import { CURRENCIES } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +import { CreditsAmount } from './CreditsAmount'; +import { CREDITS_AMOUNT, CURRENCY_AMOUNT } from './CreditsAmount.constants'; +import { creditDetails } from './CreditsAmount.mock'; + +const chooseCreditsFormSchema = createChooseCreditsFormSchema({ + creditsCap: 100, + spendingCap: 1000, +}); + +export default { + title: 'Marketplace/Molecules/CreditsAmount', + component: CreditsAmount, +} as Meta; + +type Story = StoryObj; + +const CreditsWithForm = (args: any) => { + const form = useZodForm({ + schema: chooseCreditsFormSchema, + defaultValues: { + [CURRENCY_AMOUNT]: 1, + [CREDITS_AMOUNT]: 1, + retiring: true, + }, + mode: 'onChange', + }); + return ( +
+ + + ); +}; + +export const CreditsAmountCard: Story = { + render: args => , +}; + +CreditsAmountCard.args = { + creditDetails, + paymentOption: PAYMENT_OPTIONS.CARD, + currency: CURRENCIES.usd, + setCurrency: () => {}, + setSpendingCap: () => {}, + creditsAvailable: 1000, + setCreditsAvailable: () => {}, + creditVintages: [], +}; + +export const CreditsAmountCrypto: Story = { + render: args => , +}; + +CreditsAmountCrypto.args = { + creditDetails, + paymentOption: PAYMENT_OPTIONS.CRYPTO, + currency: CURRENCIES.usd, + setCurrency: () => {}, + setSpendingCap: () => {}, + creditsAvailable: 1000, + setCreditsAvailable: () => {}, + creditVintages: [], +}; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx new file mode 100644 index 0000000000..7ed0aeee6d --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.test.tsx @@ -0,0 +1,134 @@ +import userEvent from '@testing-library/user-event'; +import { Mock } from 'vitest'; +import { PAYMENT_OPTIONS } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants'; +import { render, screen } from 'web-marketplace/test/test-utils'; + +import { CURRENCIES } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +import { CreditsAmount } from './CreditsAmount'; +import { creditDetails } from './CreditsAmount.mock'; +import { + getCreditsAvailablePerCurrency, + getCurrencyPrice, +} from './CreditsAmount.utils'; + +vi.mock('./CreditsAmount.utils', () => ({ + getCurrencyPrice: vi.fn(), + getCreditsAvailablePerCurrency: vi.fn(), +})); + +describe('CreditsAmount', () => { + const formDefaultValues = { + creditDetails, + paymentOption: PAYMENT_OPTIONS.CARD, + currency: CURRENCIES.usd, + setCurrency: () => {}, + setSpendingCap: () => {}, + creditsAvailable: 1000, + setCreditsAvailable: () => {}, + creditVintages: [], + }; + + beforeEach(() => { + (getCurrencyPrice as Mock).mockReset(); + }); + + it('renders without crashing', () => { + render(, { + formDefaultValues, + }); + + expect(screen.getByText(/Amount/i)).toBeInTheDocument(); + }); + + it('updates credits amount', async () => { + render(, { + formDefaultValues, + }); + + const creditsInput = screen.getByLabelText(/Credits Input/i); + userEvent.clear(creditsInput); + await userEvent.type(creditsInput, '10'); + expect(creditsInput).toHaveValue(10); + }); + + it('updates currency amount', async () => { + render(, { + formDefaultValues, + }); + + const currencyInput = screen.getByLabelText(/Currency Input/i); + userEvent.clear(currencyInput); + await userEvent.type(currencyInput, '50'); + expect(currencyInput).toHaveValue(50); + }); + + it('updates currency amount when credits amount changes', async () => { + (getCurrencyPrice as Mock).mockReturnValue(2); + render(, { + formDefaultValues, + }); + + const creditsInput = screen.getByLabelText(/Credits Input/i); + const currencyInput = screen.getByLabelText(/Currency Input/i); + + userEvent.clear(creditsInput); + await userEvent.type(creditsInput, '50'); + + expect(currencyInput).toHaveValue(100); + }); + + it('updates credits amount when currency amount changes', async () => { + (getCurrencyPrice as Mock).mockReturnValue(1); + render(, { + formDefaultValues, + }); + + const creditsInput = screen.getByLabelText(/Credits Input/i); + const currencyInput = screen.getByLabelText(/Currency Input/i); + + userEvent.clear(currencyInput); + await userEvent.type(currencyInput, '50'); + + expect(creditsInput).toHaveValue(50); + }); + + it('updates credits amount when max credits is selected', async () => { + (getCreditsAvailablePerCurrency as Mock).mockReturnValue( + creditDetails[0].availableCredits, + ); + render(, { + formDefaultValues, + }); + + const creditsInput = screen.getByLabelText(/Credits Input/i); + const maxCreditsButton = screen.getByRole('button', { + name: /Max Credits/i, + }); + + await userEvent.click(maxCreditsButton); + screen.debug(); + expect(creditsInput).toHaveValue(creditDetails[0].availableCredits); + }); + + it('updates currency amount when max credits is selected', async () => { + (getCurrencyPrice as Mock).mockReturnValue(1); + (getCreditsAvailablePerCurrency as Mock).mockReturnValue( + creditDetails[0].availableCredits, + ); + render(, { + formDefaultValues, + }); + + const currencyInput = screen.getByLabelText(/Currency Input/i); + const maxCreditsButton = screen.getByRole('button', { + name: /Max Credits/i, + }); + + await userEvent.click(maxCreditsButton); + + expect(currencyInput).toHaveValue( + creditDetails[0].availableCredits * creditDetails[0].creditPrice, + ); + }); +}); diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx new file mode 100644 index 0000000000..8ac84864ef --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.tsx @@ -0,0 +1,154 @@ +import { ChangeEvent, useCallback, useEffect, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { Trans } from '@lingui/macro'; +import { PAYMENT_OPTIONS } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants'; +import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; + +import { + CryptoCurrencies, + CURRENCIES, + Currency, +} from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +import { + CREDIT_VINTAGE_OPTIONS, + CREDITS_AMOUNT, + CURRENCY_AMOUNT, + DEFAULT_CRYPTO_CURRENCY, +} from './CreditsAmount.constants'; +import { CreditsAmountHeader } from './CreditsAmount.Header'; +import { CreditsAmountProps } from './CreditsAmount.types'; +import { + getCreditsAvailablePerCurrency, + getCurrencyPrice, + getVintageCredits, +} from './CreditsAmount.utils'; +import { CreditsInput } from './CreditsInput'; +import { CurrencyInput } from './CurrencyInput'; + +export const CreditsAmount = ({ + creditDetails, + paymentOption, + currency, + setCurrency, + setSpendingCap, + creditsAvailable, + setCreditsAvailable, + creditVintages, +}: CreditsAmountProps) => { + const [pricePerCredit, setPricePerCredit] = useState( + getCurrencyPrice(CURRENCIES.usd, creditDetails), + ); + const [maxCreditsSelected, setMaxCreditsSelected] = useState(false); + const { setValue, getValues } = useFormContext(); + + const creditVintageOptions = getValues(CREDIT_VINTAGE_OPTIONS); + + useEffect(() => { + if (creditVintageOptions && creditVintageOptions.length > 0) { + setCreditsAvailable( + getVintageCredits(creditVintageOptions, creditVintages), + ); + setSpendingCap(creditsAvailable); + } else { + setCreditsAvailable( + getCreditsAvailablePerCurrency(currency, creditDetails), + ); + } + }, [ + creditDetails, + creditVintageOptions, + creditVintages, + creditsAvailable, + currency, + setCreditsAvailable, + setSpendingCap, + ]); + + useEffect(() => { + setMaxCreditsSelected(false); + setCreditsAvailable( + getCreditsAvailablePerCurrency(currency, creditDetails), + ); + const newPrice = getCurrencyPrice(currency, creditDetails); + setPricePerCredit(newPrice); + setCurrency(currency); + }, [creditDetails, currency, setCreditsAvailable, setCurrency]); + + // Max credits set + useEffect(() => { + if (maxCreditsSelected) { + setValue(CREDITS_AMOUNT, creditsAvailable); + setValue(CURRENCY_AMOUNT, creditsAvailable * pricePerCredit); + setMaxCreditsSelected(false); + } + }, [creditsAvailable, maxCreditsSelected, pricePerCredit, setValue]); + + // Credits amount change + const handleCreditsAmountChange = useCallback( + (e: ChangeEvent) => { + const currentCreditsAmount = e.target.valueAsNumber; + setValue(CREDITS_AMOUNT, currentCreditsAmount); + + const currentCurrencyAmount = parseFloat(e.target.value) * pricePerCredit; + setValue(CURRENCY_AMOUNT, currentCurrencyAmount); + }, + [pricePerCredit, setValue], + ); + + // Currency type change + const handleCurrencyChange = useCallback( + (currency: string) => { + const newPrice = getCurrencyPrice( + currency as CryptoCurrencies, + creditDetails, + ); + setPricePerCredit(newPrice); + setCurrency(currency as Currency); + const creditsAvailablePerCurrency = getCreditsAvailablePerCurrency( + currency as Currency, + creditDetails, + ); + setCreditsAvailable(creditsAvailablePerCurrency); + }, + [creditDetails, setCreditsAvailable, setCurrency], + ); + + return ( +
+ +
+ + = + +
+ {paymentOption === PAYMENT_OPTIONS.CRYPTO && ( + + + Credit prices vary. By default the lowest priced credits will be + purchased first. + + + )} +
+ ); +}; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx new file mode 100644 index 0000000000..a71a664280 --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.types.tsx @@ -0,0 +1,38 @@ +import { ChangeEvent } from 'react'; +import { + CreditDetails, + CreditsVintages, +} from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.types'; + +import { + CryptoCurrencies, + Currency, +} from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +export type PaymentOptionsType = 'card' | 'crypto'; +export interface CreditsAmountProps { + creditDetails: CreditDetails[]; + paymentOption: PaymentOptionsType; + currency: Currency; + setCurrency: (currency: Currency) => void; + setSpendingCap: (spendingCap: number) => void; + creditsAvailable: number; + setCreditsAvailable: (creditsAvailable: number) => void; + creditVintages: CreditsVintages[]; +} + +export interface CreditsInputProps { + creditsAvailable: number; + handleCreditsAmountChange: (e: ChangeEvent) => void; + paymentOption: PaymentOptionsType; +} + +export interface CurrencyInputProps { + maxCurrencyAmount: number; + paymentOption: PaymentOptionsType; + handleCurrencyChange: (currency: CryptoCurrencies | string) => void; + defaultCryptoCurrency: CryptoCurrencies; + creditDetails: CreditDetails[]; + currency: Currency; + setCurrency: (currency: Currency) => void; +} diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx new file mode 100644 index 0000000000..f0975362cc --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils.tsx @@ -0,0 +1,37 @@ +import { + CreditDetails, + CreditsVintages, +} from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.types'; + +import { Currency } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +export const getCurrencyPrice = ( + currency: Currency, + creditDetails: CreditDetails[], +) => { + return ( + creditDetails.find(credit => credit.currency === currency)?.creditPrice || 1 + ); +}; + +export const getCreditsAvailablePerCurrency = ( + currency: Currency, + creditDetails: CreditDetails[], +) => { + return ( + creditDetails.find(credit => credit.currency === currency) + ?.availableCredits || 0 + ); +}; + +export const getVintageCredits = ( + creditVintageOptions: string[], + creditVintages: CreditsVintages[], +) => { + return creditVintageOptions.reduce((sum: number, option: string) => { + const credits = + creditVintages.find(vintage => vintage.batchDenom === option)?.credits || + '0'; + return sum + +credits; + }, 0); +}; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx new file mode 100644 index 0000000000..882c5a9e5d --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CreditsInput.tsx @@ -0,0 +1,78 @@ +import { ChangeEvent, useEffect, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { Trans } from '@lingui/macro'; +import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; + +import { LeafIcon } from 'web-components/src/components/icons/LeafIcon'; +import TextField from 'web-components/src/components/inputs/new/TextField/TextField'; + +import { CREDITS_AMOUNT } from './CreditsAmount.constants'; +import { CreditsInputProps } from './CreditsAmount.types'; + +export const CreditsInput = ({ + creditsAvailable, + handleCreditsAmountChange, + paymentOption, +}: CreditsInputProps) => { + const [maxCreditsAvailable, setMaxCreditsAvailable] = + useState(creditsAvailable); + const [isFocused, setIsFocused] = useState(false); + const { + setValue, + register, + formState: { errors }, + } = useFormContext(); + const { onChange, onBlur, name, ref } = register(CREDITS_AMOUNT); + + const onHandleFocus = () => setIsFocused(true); + const onHandleBlur = (event: { target: any; type?: any }) => { + setIsFocused(false); + onBlur(event); + }; + + useEffect(() => { + setMaxCreditsAvailable(creditsAvailable); + }, [creditsAvailable, paymentOption, setValue]); + + const onHandleChange = (event: ChangeEvent) => { + handleCreditsAmountChange(event); + onChange(event); + }; + + return ( +
+ + credits + + } + /> + {errors[CREDITS_AMOUNT] && ( +
+ {`${errors[CREDITS_AMOUNT].message}`} +
+ )} +
+ ); +}; diff --git a/web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx b/web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx new file mode 100644 index 0000000000..890011eff6 --- /dev/null +++ b/web-marketplace/src/components/molecules/CreditsAmount/CurrencyInput.tsx @@ -0,0 +1,141 @@ +import { ChangeEvent, lazy, useCallback, useState } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { PAYMENT_OPTIONS } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants'; +import { ChooseCreditsFormSchemaType } from 'web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema'; + +import { DenomIconWithCurrency } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency'; +import { + CURRENCIES, + Currency, +} from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import TextField from 'web-components/src/components/inputs/new/TextField/TextField'; + +import { CREDITS_AMOUNT, CURRENCY_AMOUNT } from './CreditsAmount.constants'; +import { CurrencyInputProps } from './CreditsAmount.types'; +import { getCurrencyPrice } from './CreditsAmount.utils'; + +const CustomSelect = lazy( + () => + import( + 'web-components/src/components/inputs/new/CustomSelect/CustomSelect' + ), +); + +export const CurrencyInput = ({ + maxCurrencyAmount, + paymentOption, + handleCurrencyChange, + defaultCryptoCurrency, + creditDetails, + currency, + setCurrency, +}: CurrencyInputProps) => { + const { + register, + setValue, + formState: { errors }, + } = useFormContext(); + const { onChange, onBlur, name, ref } = register(CURRENCY_AMOUNT); + const [isFocused, setIsFocused] = useState(false); + + const handleOnFocus = () => setIsFocused(true); + const handleOnBlur = (event: { target: any; type?: any }) => { + setIsFocused(false); + onBlur(event); + }; + const handleOnChange = useCallback( + (event: ChangeEvent) => { + const value = event.target.valueAsNumber; + const creditsQty = + value / getCurrencyPrice(CURRENCIES[currency], creditDetails); + setValue(CREDITS_AMOUNT, creditsQty); + onChange(event); + }, + [creditDetails, currency, onChange, setValue], + ); + + const onHandleCurrencyChange = useCallback( + (currency: string) => { + handleCurrencyChange(currency); + setCurrency(currency as Currency); + }, + [handleCurrencyChange, setCurrency], + ); + + return ( +
+ {paymentOption === PAYMENT_OPTIONS.CARD && ( + $ + )} + + ) : ( + currency !== CURRENCIES.usd) + .map(currency => ({ + component: { + label: currency, + element: () => ( + + ), + }, + }))} + onSelect={onHandleCurrencyChange} + defaultOption={defaultCryptoCurrency} + /> + ) + } + /> + {errors[CURRENCY_AMOUNT]?.message && ( +
+ {`${errors[CURRENCY_AMOUNT].message} ${currency.toUpperCase()}`} +
+ )} +
+ ); +}; diff --git a/web-marketplace/src/components/molecules/DenomIcon/DenomIcon.tsx b/web-marketplace/src/components/molecules/DenomIcon/DenomIcon.tsx index c1df354c23..78d14e11ce 100644 --- a/web-marketplace/src/components/molecules/DenomIcon/DenomIcon.tsx +++ b/web-marketplace/src/components/molecules/DenomIcon/DenomIcon.tsx @@ -6,28 +6,54 @@ import { EVMOS_DENOM, GRAVITY_USDC_DENOM, REGEN_DENOM, -} from 'config/allowedBaseDenoms'; + USD_DENOM, + USDC_DENOM, + USDCAXL_DENOM, +} from 'web-marketplace/src/config/allowedBaseDenoms'; import AxlUsdcIcon from 'web-components/src/components/icons/coins/AxlUsdcIcon'; import EeurIcon from 'web-components/src/components/icons/coins/EeurIcon'; import EvmosIcon from 'web-components/src/components/icons/coins/EvmosIcon'; import GravUsdcIcon from 'web-components/src/components/icons/coins/GravUsdcIcon'; +import USFlagIcon from 'web-components/src/components/icons/flags/USFlagIcon'; import { RegenTokenIcon } from 'web-components/src/components/icons/RegenTokenIcon'; export interface Props { baseDenom?: string; sx?: SxProps; iconSx?: SxProps; + className?: string; } -const DenomIcon = ({ baseDenom, sx = [], iconSx }: Props): JSX.Element => { +const DenomIcon = ({ + baseDenom, + sx = [], + iconSx, + className = '', +}: Props): JSX.Element => { return ( - {baseDenom === GRAVITY_USDC_DENOM && } - {baseDenom === AXELAR_USDC_DENOM && } - {baseDenom === EEUR_DENOM && } - {baseDenom === REGEN_DENOM && } - {baseDenom === EVMOS_DENOM && } + {baseDenom === GRAVITY_USDC_DENOM || + (baseDenom === USDC_DENOM && ( + + ))} + {baseDenom === AXELAR_USDC_DENOM && ( + + )} + {baseDenom === EEUR_DENOM && ( + + )} + {baseDenom === REGEN_DENOM && ( + + )} + {baseDenom === EVMOS_DENOM && ( + + )} + {baseDenom === USD_DENOM && } + + {baseDenom === USDCAXL_DENOM && ( + + )} ); }; diff --git a/web-marketplace/src/components/molecules/Form/Form.tsx b/web-marketplace/src/components/molecules/Form/Form.tsx index 598f8a2663..b71884e345 100644 --- a/web-marketplace/src/components/molecules/Form/Form.tsx +++ b/web-marketplace/src/components/molecules/Form/Form.tsx @@ -7,11 +7,10 @@ import { } from 'react-hook-form'; import { DevTool } from '@hookform/devtools'; import { Box, SxProps } from '@mui/material'; -import { sxToArray } from 'utils/mui/sxToArray'; +import { IS_DEV } from 'web-marketplace/src/lib/env'; import { Theme } from 'web-components/src/theme/muiTheme'; - -import { IS_DEV } from 'lib/env'; +import { sxToArray } from 'web-components/src/utils/mui/sxToArray'; export type FormRef = MutableRefObject< | { submitForm: (draft?: boolean) => void; isFormValid: () => boolean } diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx new file mode 100644 index 0000000000..d714bdf608 --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { Trans } from '@lingui/macro'; +import { Link } from 'web-marketplace/src/components/atoms'; +import { CREDIT_VINTAGE_OPTIONS } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants'; + +import { TextButton } from 'web-components/src/components/buttons/TextButton'; +import CheckboxLabel from 'web-components/src/components/inputs/new/CheckboxLabel/CheckboxLabel'; + +import { ChooseCreditsFormSchemaType } from './ChooseCreditsForm.schema'; + +export function AdvanceSettings({ + advanceSettingsOpen, + toggleAdvancedSettings, + creditVintages, + handleCreditVintageOptions, +}: { + advanceSettingsOpen: boolean; + toggleAdvancedSettings: (e: React.MouseEvent) => void; + creditVintages: { date: string; credits: string; batchDenom: string }[]; + handleCreditVintageOptions: (e: React.ChangeEvent) => void; +}) { + const { register, getValues } = useFormContext(); + const creditVintageOptions = getValues(CREDIT_VINTAGE_OPTIONS); + + return ( +
+
+ + + {advanceSettingsOpen ? '-' : '+'} + + Advanced settings + + {advanceSettingsOpen && ( +
+

+ Choose specific credit vintages{' '} + + ( + + by default the cheapest credit vintage will be purchased first + + ) + +

+ {creditVintages.map(({ date, credits, batchDenom }) => ( +
+ +

+ + {credits} credits available + + | + + view batch » + +

+
+ ))} +
+ )} +
+
+ ); +} diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx new file mode 100644 index 0000000000..5351c43902 --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx @@ -0,0 +1,68 @@ +import { useFormContext } from 'react-hook-form'; +import { Trans } from '@lingui/macro'; +import { useLingui } from '@lingui/react'; +import { + cryptoOptions, + RETIRING, +} from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants'; + +import { Radio } from 'web-components/src/components/inputs/new/Radio/Radio'; +import { RadioGroup } from 'web-components/src/components/inputs/new/RadioGroup/RadioGroup'; +import { Title } from 'web-components/src/components/typography/Title'; + +import { ChooseCreditsFormSchemaType } from './ChooseCreditsForm.schema'; + +export function CryptoOptions({ + retiring, + handleCryptoPurchaseOptions, +}: { + retiring: boolean; + handleCryptoPurchaseOptions: () => void; +}) { + const { register } = useFormContext(); + const { _ } = useLingui(); + return ( +
+ + <Trans>Crypto purchase options</Trans> + +

+ + Credits purchased with crypto can be purchased in either a retired or + tradable state. + +

+ + {cryptoOptions.map(({ label, description, linkTo, value }) => ( + + {_(label)} + + } + description={ +

+ {_(description)} + + Learn more » + +

+ } + /> + ))} +
+
+ ); +} diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx new file mode 100644 index 0000000000..8321b59da6 --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx @@ -0,0 +1,93 @@ +import { ChangeEvent, useState } from 'react'; +import { Trans } from '@lingui/macro'; + +import CreditCardIcon from 'web-components/src/components/icons/CreditCardIcon'; +import CryptoIcon from 'web-components/src/components/icons/CryptoIcon'; + +import { PAYMENT_OPTIONS } from './ChooseCreditsForm.constants'; +import { + ChooseCreditButtonProps, + PaymentOptionsType, +} from './ChooseCreditsForm.types'; + +function ChooseCreditButton({ + children, + value, + isChecked, + onChange, +}: ChooseCreditButtonProps) { + return ( + + ); +} + +function ChooseCreditButtonGroup({ + onSelectOption, +}: { + onSelectOption: (option: PaymentOptionsType) => void; +}) { + const [selectedButton, setSelectedButton] = useState( + PAYMENT_OPTIONS.CARD, + ); + + const handleButtonClick = (e: ChangeEvent) => { + const paymentType = e.target.value as PaymentOptionsType; + setSelectedButton(paymentType); + onSelectOption(paymentType); + }; + + return ( +
+ + +
+ + buy + {' '} + with credit card +
+
+ + +
+ + buy + {' '} + with crypto +
+
+
+ ); +} + +export const PaymentOptions = ({ + setPaymentOption, +}: { + setPaymentOption: (option: PaymentOptionsType) => void; +}) => { + return ; +}; diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx new file mode 100644 index 0000000000..0339cbe64b --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx @@ -0,0 +1,10 @@ +import { msg } from '@lingui/macro'; + +export const PAYMENT_OPTIONS = { + CARD: 'card', + CRYPTO: 'crypto', +} as const; + +export const MAX_AMOUNT = msg`Amount cannot exceed`; +export const MAX_CREDITS = msg`Credits cannot exceed`; +export const POSITIVE_NUMBER = msg`Must be positive`; diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx new file mode 100644 index 0000000000..2ca280fa7d --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.schema.tsx @@ -0,0 +1,43 @@ +import { i18n } from '@lingui/core'; +import { + CREDITS_AMOUNT, + CURRENCY_AMOUNT, +} from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants'; +import { z } from 'zod'; + +import { + MAX_AMOUNT, + MAX_CREDITS, + POSITIVE_NUMBER, +} from './ChooseCreditsForm.constants'; + +export const createChooseCreditsFormSchema = ({ + creditsCap, + spendingCap, +}: { + creditsCap: number; + spendingCap: number; +}) => { + return z.object({ + [CURRENCY_AMOUNT]: z.coerce + .number() + .positive(POSITIVE_NUMBER) + .max( + spendingCap, + `${i18n._(MAX_AMOUNT)} ${spendingCap.toLocaleString(undefined, { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + })}`, + ), + [CREDITS_AMOUNT]: z.coerce + .number() + .positive(POSITIVE_NUMBER) + .max(creditsCap, `${i18n._(MAX_CREDITS)} ${creditsCap}`), + retiring: z.boolean(), + creditVintageOptions: z.array(z.string()), + }); +}; + +export type ChooseCreditsFormSchemaType = z.infer< + ReturnType +>; diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx new file mode 100644 index 0000000000..fddd3196da --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.stories.tsx @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { + creditDetails, + creditVintages, +} from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.mock'; + +import { ChooseCreditsForm } from './ChooseCreditsForm'; + +export default { + title: 'Marketplace/Organisms/ChooseCreditsForm', + component: ChooseCreditsForm, +} as Meta; + +type Story = StoryObj; + +export const ChooseCredits: Story = { + render: args => , +}; + +ChooseCredits.args = { + creditVintages, + creditDetails, +}; diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx new file mode 100644 index 0000000000..bec8a6b75e --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.test.tsx @@ -0,0 +1,53 @@ +import { + creditDetails, + creditVintages, +} from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.mock'; +import { render, screen, userEvent } from 'web-marketplace/test/test-utils'; + +import { ChooseCreditsForm } from './ChooseCreditsForm'; + +describe('ChooseCreditsForm', () => { + it('renders without crashing', () => { + render( + , + ); + + expect(screen.getByTestId('choose-credits-form')).toBeInTheDocument(); + }); + + it('opens and closes advanced settings', () => { + render( + , + ); + + const advancedSettingsButton = screen.getByRole('button', { + name: /advanced settings/i, + }); + + userEvent.click(advancedSettingsButton); + expect(screen.getByText(/advanced settings/i)).toBeInTheDocument(); + + userEvent.click(advancedSettingsButton); + expect(screen.queryByTestId('advanced-settings')).not.toBeInTheDocument(); + }); + + it('selects card payment option', () => { + render( + , + ); + const cardOption = screen.getByRole('radio', { + name: /card/i, + }); + userEvent.click(cardOption); + expect(cardOption).toBeChecked(); + }); +}); diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx new file mode 100644 index 0000000000..9c33412d47 --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.tsx @@ -0,0 +1,208 @@ +import { + ChangeEvent, + MouseEvent, + Suspense, + useCallback, + useEffect, + useState, +} from 'react'; +import { SubmitHandler, useWatch } from 'react-hook-form'; +import { CreditsAmount } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount'; +import { + CREDIT_VINTAGE_OPTIONS, + CREDITS_AMOUNT, + CURRENCY_AMOUNT, + RETIRING, +} from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.constants'; +import { getCreditsAvailablePerCurrency } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils'; +import Form from 'web-marketplace/src/components/molecules/Form/Form'; +import { useZodForm } from 'web-marketplace/src/components/molecules/Form/hook/useZodForm'; + +import Card from 'web-components/src/components/cards/Card'; +import { + CURRENCIES, + Currency, +} from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; +import { Loading } from 'web-components/src/components/loading'; + +import { AdvanceSettings } from './ChooseCreditsForm.AdvanceSettings'; +import { PAYMENT_OPTIONS } from './ChooseCreditsForm.constants'; +import { CryptoOptions } from './ChooseCreditsForm.CryptoOptions'; +import { PaymentOptions } from './ChooseCreditsForm.PaymentOptions'; +import { + ChooseCreditsFormSchemaType, + createChooseCreditsFormSchema, +} from './ChooseCreditsForm.schema'; +import { + CreditDetails, + CreditsVintages, + PaymentOptionsType, +} from './ChooseCreditsForm.types'; +import { getSpendingCap } from './ChooseCreditsForm.utils'; + +export function ChooseCreditsForm({ + creditVintages, + creditDetails, +}: { + creditVintages: CreditsVintages[]; + creditDetails: CreditDetails[]; +}) { + /** TODO + * + * 1. Update available creditVintages when currency changes. + * Other option would be to simply append to each creditDetails a list of available creditVintages + * and the sum of those vintages credits would be equal to the creditDetails.availableCredits. + * + * 2. For crypto purchase, we also need to know whether sold credits are tradable or not, because + * if the user picks up "Buy tradable ecocredits" option then we don't want to show credits + * for sell that are not tradable. + * + * 3. Implement Advance Settings functionality. + * + */ + + const [paymentOption, setPaymentOption] = useState( + PAYMENT_OPTIONS.CARD, + ); + const [advanceSettingsOpen, setAdvanceSettingsOpen] = useState(false); + + const [spendingCap, setSpendingCap] = useState( + getSpendingCap(CURRENCIES.usd, creditDetails), + ); + const [currency, setCurrency] = useState(CURRENCIES.usd); + + const [creditsAvailable, setCreditsAvailable] = useState( + getCreditsAvailablePerCurrency(currency, creditDetails), + ); + + const chooseCreditsFormSchema = createChooseCreditsFormSchema({ + creditsCap: creditDetails.find(credit => credit.currency === currency) + ?.availableCredits!, + spendingCap, + }); + + const form = useZodForm({ + schema: chooseCreditsFormSchema, + defaultValues: { + [CURRENCY_AMOUNT]: 0, + [CREDITS_AMOUNT]: 0, + [RETIRING]: true, + }, + mode: 'onChange', + }); + + const retiring = useWatch({ + control: form.control, + name: 'retiring', + }); + + const creditVintageOptions = useWatch({ + control: form.control, + name: CREDIT_VINTAGE_OPTIONS, + }); + + useEffect(() => { + if (!advanceSettingsOpen) { + form.setValue(CREDIT_VINTAGE_OPTIONS, []); + } + }, [advanceSettingsOpen, form]); + + useEffect(() => { + form.reset({ + [CURRENCY_AMOUNT]: 0, + [CREDITS_AMOUNT]: 0, + [RETIRING]: retiring, + [CREDIT_VINTAGE_OPTIONS]: form.getValues(CREDIT_VINTAGE_OPTIONS) || [], + }); + }, [form, spendingCap, retiring]); + + useEffect(() => { + setSpendingCap(getSpendingCap(currency, creditDetails)); + }, [creditDetails, currency]); + + const handleOnSubmit: SubmitHandler = + useCallback(data => { + // TO-DO + }, []); + + const handleCryptoPurchaseOptions = useCallback(() => { + form.setValue('retiring', !retiring); + }, [form, retiring]); + + const handleCreditVintageOptions = useCallback( + (e: ChangeEvent) => { + const value = e.target.value; + const checked = e.target.checked; + const currentValues = creditVintageOptions || []; + const updatedValues = checked + ? [...currentValues, value] + : currentValues.filter(item => item !== value); + + form.setValue(CREDIT_VINTAGE_OPTIONS, updatedValues); + }, + [creditVintageOptions, form], + ); + + const handlePaymentOptions = useCallback( + (option: string) => { + setPaymentOption(option as PaymentOptionsType); + form.setValue(CREDIT_VINTAGE_OPTIONS, []); + if (option === PAYMENT_OPTIONS.CRYPTO) { + setCurrency(CURRENCIES.uregen); + setSpendingCap(getSpendingCap(CURRENCIES.uregen, creditDetails)); + setCreditsAvailable( + getCreditsAvailablePerCurrency(CURRENCIES.uregen, creditDetails), + ); + } + if (option === PAYMENT_OPTIONS.CARD) { + setCurrency(CURRENCIES.usd); + setSpendingCap(getSpendingCap(CURRENCIES.usd, creditDetails)); + setCreditsAvailable( + getCreditsAvailablePerCurrency(CURRENCIES.usd, creditDetails), + ); + } + }, + [creditDetails, form], + ); + + const toggleAdvancedSettings = useCallback((e: MouseEvent) => { + e.preventDefault(); + setAdvanceSettingsOpen(prev => !prev); + }, []); + + return ( + }> + +
+ + + {paymentOption === PAYMENT_OPTIONS.CRYPTO && ( + + )} + + +
+
+ ); +} diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.types.tsx b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.types.tsx new file mode 100644 index 0000000000..5b4055f31f --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.types.tsx @@ -0,0 +1,24 @@ +import { ChangeEvent, ReactNode } from 'react'; + +import { Currency } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +export type PaymentOptionsType = 'card' | 'crypto'; + +export interface ChooseCreditButtonProps { + children: ReactNode; + value: string; + isChecked: boolean; + onChange: (e: ChangeEvent) => void; +} + +export interface CreditDetails { + availableCredits: number; + currency: Currency; + creditPrice: number; +} + +export interface CreditsVintages { + date: string; + credits: string; + batchDenom: string; +} diff --git a/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.utils.ts b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.utils.ts new file mode 100644 index 0000000000..2fc217faee --- /dev/null +++ b/web-marketplace/src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.utils.ts @@ -0,0 +1,15 @@ +import { getCurrencyPrice } from 'web-marketplace/src/components/molecules/CreditsAmount/CreditsAmount.utils'; + +import { Currency } from 'web-components/src/components/DenomIconWithCurrency/DenomIconWithCurrency.constants'; + +import { CreditDetails } from './ChooseCreditsForm.types'; + +export function getSpendingCap( + currency: Currency, + creditsDetails: CreditDetails[], +) { + return ( + getCurrencyPrice(currency, creditsDetails) * + creditsDetails.find(item => item.currency === currency)!.availableCredits + ); +} diff --git a/web-marketplace/src/config/allowedBaseDenoms.ts b/web-marketplace/src/config/allowedBaseDenoms.ts index 4a5a45e94a..740151bfaa 100644 --- a/web-marketplace/src/config/allowedBaseDenoms.ts +++ b/web-marketplace/src/config/allowedBaseDenoms.ts @@ -7,6 +7,9 @@ export const AXELAR_USDC_DENOM = isRedwood ? 'uausdc' : 'uusdc'; export const EEUR_DENOM = 'eeur'; export const REGEN_DENOM = 'uregen'; export const EVMOS_DENOM = 'atevmos'; +export const USD_DENOM = 'usd'; +export const USDC_DENOM = 'usdc'; +export const USDCAXL_DENOM = 'usdcaxl'; export const USD_DENOMS = [GRAVITY_USDC_DENOM, AXELAR_USDC_DENOM]; export const EUR_DENOMS = [EEUR_DENOM]; diff --git a/web-marketplace/src/jest.mock.ts b/web-marketplace/src/jest.mock.ts deleted file mode 100644 index 73a5967447..0000000000 --- a/web-marketplace/src/jest.mock.ts +++ /dev/null @@ -1,13 +0,0 @@ -window.matchMedia = jest.fn().mockImplementation(query => { - return { - matches: false, - media: query, - onchange: null, - addListener: jest.fn(), - removeListener: jest.fn(), - }; -}); - -window.scrollTo = jest.fn(); - -export {}; diff --git a/web-marketplace/src/lib/i18n/locales/en.po b/web-marketplace/src/lib/i18n/locales/en.po index 525244f1aa..92cff2396a 100644 --- a/web-marketplace/src/lib/i18n/locales/en.po +++ b/web-marketplace/src/lib/i18n/locales/en.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2024-09-11 09:33+0200\n" +"POT-Creation-Date: 2024-09-11 08:50+0100\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -175,6 +175,10 @@ msgstr "" msgid "Admin" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:37 +msgid "Advanced settings" +msgstr "" + #: src/pages/BatchDetails/BatchDetails.tsx:122 msgid "All credits" msgstr "" @@ -205,6 +209,7 @@ msgstr "" msgid "amount" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx:31 #: src/components/organisms/BasketEcocreditsTable/BasketEcocreditsTable.tsx:62 #: src/components/organisms/BridgeForm/BridgeForm.tsx:103 #: src/lib/constants/shared.constants.tsx:27 @@ -227,6 +232,10 @@ msgstr "" msgid "Amount bridged is the same as amount cancelled in the ledger documentation" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx:8 +msgid "Amount cannot exceed" +msgstr "" + #: src/pages/Dashboard/MyEcocredits/hooks/useCreateSellOrderSubmit.tsx:150 #: src/pages/Marketplace/Storefront/hooks/useBuySellOrderSubmit.tsx:208 msgid "amount of credits" @@ -427,6 +436,11 @@ msgstr "" msgid "buffer pool accounts" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:65 +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:78 +msgid "buy" +msgstr "" + #: src/pages/Marketplace/Storefront/Storefront.constants.ts:4 msgid "Buy" msgstr "" @@ -469,6 +483,7 @@ msgstr "" msgid "buy NCT" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:20 #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:438 msgid "Buy tradable ecocredits" msgstr "" @@ -482,6 +497,10 @@ msgstr "" msgid "By connecting to Regen Marketplace, you agree to our <0>Terms of Service and <1>Privacy Policy" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:49 +msgid "by default the cheapest credit vintage will be purchased first" +msgstr "" + #: src/components/organisms/CreditSendForm/CreditSendForm.tsx:137 msgid "By default these credits are tradable but you may check “retire all credits upon transfer” below to automatically retire them upon sending." msgstr "" @@ -588,6 +607,10 @@ msgstr "" msgid "Choose Project" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:46 +msgid "Choose specific credit vintages" +msgstr "" + #: src/components/organisms/MediaForm/MediaForm.constants.ts:6 msgid "Choose the photo that will show at the top of the project page and in project preview cards." msgstr "" @@ -823,6 +846,10 @@ msgstr "" msgid "credit name" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.tsx:146 +msgid "Credit prices vary. By default the lowest priced credits will be purchased first." +msgstr "" + #: src/components/organisms/AgreePurchaseForm/AgreePurchaseForm.Retirement.tsx:86 #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:515 msgid "Credit retirement location" @@ -845,10 +872,19 @@ msgstr "" msgid "Credit unit definition" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsInput.tsx:67 +msgid "credits" +msgstr "" + #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:216 msgid "Credits" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx:45 +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:71 +msgid "credits available" +msgstr "" + #: src/pages/Projects/AllProjects/AllProjects.config.ts:18 msgid "Credits available - high to low" msgstr "" @@ -861,6 +897,10 @@ msgstr "" msgid "Credits Cancelled" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx:9 +msgid "Credits cannot exceed" +msgstr "" + #: src/features/ecocredit/CreateBatchBySteps/form-model.ts:47 msgid "Credits have been issued!" msgstr "" @@ -869,6 +909,10 @@ msgstr "" msgid "Credits issued" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx:30 +msgid "Credits purchased with crypto can be purchased in either a retired or tradable state." +msgstr "" + #: src/components/molecules/BatchTotalsGrid.tsx:46 #: src/components/molecules/ProjectBatchTotals/ProjectBatchTotals.tsx:94 msgid "Credits Retired" @@ -891,6 +935,10 @@ msgstr "" msgid "Crypto Organization" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx:27 +msgid "Crypto purchase options" +msgstr "" + #: src/components/organisms/ConnectWalletFlow/ConnectWalletFlow.constants.ts:14 msgid "current account" msgstr "" @@ -1267,6 +1315,10 @@ msgstr "" msgid "Impact" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx:50 +msgid "in" +msgstr "" + #: src/components/organisms/DescriptionForm/DescriptionForm.constants.ts:15 msgid "In one sentence, summarize the story above." msgstr "" @@ -1357,6 +1409,10 @@ msgstr "" msgid "learn more" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx:59 +msgid "Learn more" +msgstr "" + #: src/components/organisms/RegistryLayout/RegistryLayout.Footer.tsx:56 #: src/pages/Home/Home.tsx:196 msgid "Learn More" @@ -1529,6 +1585,10 @@ msgstr "" msgid "MsgCreateProject Error" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx:10 +msgid "Must be positive" +msgstr "" + #: src/features/ecocredit/CreateBatchBySteps/CreateBatchMultiStepForm/CreateBatchMultiStepForm.constants.ts:3 msgid "Must have recipients" msgstr "" @@ -2114,6 +2174,7 @@ msgstr "" msgid "Retire all credits upon transfer" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:13 #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:416 msgid "Retire credits now" msgstr "" @@ -2557,10 +2618,18 @@ msgstr "" msgid "These credits will be a tradable asset. They can be retired later via Regen Marketplace." msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:21 +msgid "These credits will be a tradeable asset. They can be retired later via Regen Marketplace." +msgstr "" + #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:419 msgid "These credits will be retired upon purchase and will not be tradable. Retirement is permanent and non-reversible." msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:14 +msgid "These credits will be retired upon purchase and will not be tradeable. Retirement is permanent and non-reversible." +msgstr "" + #: src/components/organisms/RolesForm/components/AdminModal/AdminModal.tsx:71 msgid "This change will only be made after you save the change and sign the transaction." msgstr "" @@ -2820,6 +2889,10 @@ msgstr "" msgid "View all supported file types»" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:79 +msgid "view batch" +msgstr "" + #: src/components/organisms/Portfolio/Portfolio.constants.ts:3 msgid "View Certificate" msgstr "" @@ -2932,6 +3005,14 @@ msgstr "" msgid "whoops! Cancel" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:67 +msgid "with credit card" +msgstr "" + +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:80 +msgid "with crypto" +msgstr "" + #: src/components/organisms/PostForm/PostForm.tsx:241 msgid "Write a short comment or longer project update." msgstr "" diff --git a/web-marketplace/src/lib/i18n/locales/es.po b/web-marketplace/src/lib/i18n/locales/es.po index c2e001adf7..ea05c64e25 100644 --- a/web-marketplace/src/lib/i18n/locales/es.po +++ b/web-marketplace/src/lib/i18n/locales/es.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"POT-Creation-Date: 2024-09-11 09:33+0200\n" +"POT-Creation-Date: 2024-09-11 08:50+0100\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -175,6 +175,10 @@ msgstr "" msgid "Admin" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:37 +msgid "Advanced settings" +msgstr "" + #: src/pages/BatchDetails/BatchDetails.tsx:122 msgid "All credits" msgstr "" @@ -205,6 +209,7 @@ msgstr "" msgid "amount" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx:31 #: src/components/organisms/BasketEcocreditsTable/BasketEcocreditsTable.tsx:62 #: src/components/organisms/BridgeForm/BridgeForm.tsx:103 #: src/lib/constants/shared.constants.tsx:27 @@ -227,6 +232,10 @@ msgstr "" msgid "Amount bridged is the same as amount cancelled in the ledger documentation" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx:8 +msgid "Amount cannot exceed" +msgstr "" + #: src/pages/Dashboard/MyEcocredits/hooks/useCreateSellOrderSubmit.tsx:150 #: src/pages/Marketplace/Storefront/hooks/useBuySellOrderSubmit.tsx:208 msgid "amount of credits" @@ -427,6 +436,11 @@ msgstr "" msgid "buffer pool accounts" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:65 +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:78 +msgid "buy" +msgstr "" + #: src/pages/Marketplace/Storefront/Storefront.constants.ts:4 msgid "Buy" msgstr "" @@ -469,6 +483,7 @@ msgstr "" msgid "buy NCT" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:20 #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:438 msgid "Buy tradable ecocredits" msgstr "" @@ -482,6 +497,10 @@ msgstr "" msgid "By connecting to Regen Marketplace, you agree to our <0>Terms of Service and <1>Privacy Policy" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:49 +msgid "by default the cheapest credit vintage will be purchased first" +msgstr "" + #: src/components/organisms/CreditSendForm/CreditSendForm.tsx:137 msgid "By default these credits are tradable but you may check “retire all credits upon transfer” below to automatically retire them upon sending." msgstr "" @@ -588,6 +607,10 @@ msgstr "" msgid "Choose Project" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:46 +msgid "Choose specific credit vintages" +msgstr "" + #: src/components/organisms/MediaForm/MediaForm.constants.ts:6 msgid "Choose the photo that will show at the top of the project page and in project preview cards." msgstr "" @@ -823,6 +846,10 @@ msgstr "" msgid "credit name" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.tsx:146 +msgid "Credit prices vary. By default the lowest priced credits will be purchased first." +msgstr "" + #: src/components/organisms/AgreePurchaseForm/AgreePurchaseForm.Retirement.tsx:86 #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:515 msgid "Credit retirement location" @@ -845,10 +872,19 @@ msgstr "" msgid "Credit unit definition" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsInput.tsx:67 +msgid "credits" +msgstr "" + #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:216 msgid "Credits" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx:45 +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:71 +msgid "credits available" +msgstr "" + #: src/pages/Projects/AllProjects/AllProjects.config.ts:18 msgid "Credits available - high to low" msgstr "" @@ -861,6 +897,10 @@ msgstr "" msgid "Credits Cancelled" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx:9 +msgid "Credits cannot exceed" +msgstr "" + #: src/features/ecocredit/CreateBatchBySteps/form-model.ts:47 msgid "Credits have been issued!" msgstr "" @@ -869,6 +909,10 @@ msgstr "" msgid "Credits issued" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx:30 +msgid "Credits purchased with crypto can be purchased in either a retired or tradable state." +msgstr "" + #: src/components/molecules/BatchTotalsGrid.tsx:46 #: src/components/molecules/ProjectBatchTotals/ProjectBatchTotals.tsx:94 msgid "Credits Retired" @@ -891,6 +935,10 @@ msgstr "" msgid "Crypto Organization" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx:27 +msgid "Crypto purchase options" +msgstr "" + #: src/components/organisms/ConnectWalletFlow/ConnectWalletFlow.constants.ts:14 msgid "current account" msgstr "" @@ -1267,6 +1315,10 @@ msgstr "" msgid "Impact" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.Header.tsx:50 +msgid "in" +msgstr "" + #: src/components/organisms/DescriptionForm/DescriptionForm.constants.ts:15 msgid "In one sentence, summarize the story above." msgstr "" @@ -1357,6 +1409,10 @@ msgstr "" msgid "learn more" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.CryptoOptions.tsx:59 +msgid "Learn more" +msgstr "" + #: src/components/organisms/RegistryLayout/RegistryLayout.Footer.tsx:56 #: src/pages/Home/Home.tsx:196 msgid "Learn More" @@ -1529,6 +1585,10 @@ msgstr "" msgid "MsgCreateProject Error" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.constants.tsx:10 +msgid "Must be positive" +msgstr "" + #: src/features/ecocredit/CreateBatchBySteps/CreateBatchMultiStepForm/CreateBatchMultiStepForm.constants.ts:3 msgid "Must have recipients" msgstr "" @@ -2114,6 +2174,7 @@ msgstr "" msgid "Retire all credits upon transfer" msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:13 #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:416 msgid "Retire credits now" msgstr "" @@ -2557,10 +2618,18 @@ msgstr "" msgid "These credits will be a tradable asset. They can be retired later via Regen Marketplace." msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:21 +msgid "These credits will be a tradeable asset. They can be retired later via Regen Marketplace." +msgstr "" + #: src/components/organisms/BuyCreditsModal/BuyCreditsModal.tsx:419 msgid "These credits will be retired upon purchase and will not be tradable. Retirement is permanent and non-reversible." msgstr "" +#: src/components/molecules/CreditsAmount/CreditsAmount.constants.ts:14 +msgid "These credits will be retired upon purchase and will not be tradeable. Retirement is permanent and non-reversible." +msgstr "" + #: src/components/organisms/RolesForm/components/AdminModal/AdminModal.tsx:71 msgid "This change will only be made after you save the change and sign the transaction." msgstr "" @@ -2820,6 +2889,10 @@ msgstr "" msgid "View all supported file types»" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.AdvanceSettings.tsx:79 +msgid "view batch" +msgstr "" + #: src/components/organisms/Portfolio/Portfolio.constants.ts:3 msgid "View Certificate" msgstr "" @@ -2932,6 +3005,14 @@ msgstr "" msgid "whoops! Cancel" msgstr "" +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:67 +msgid "with credit card" +msgstr "" + +#: src/components/organisms/ChooseCreditsForm/ChooseCreditsForm.PaymentOptions.tsx:80 +msgid "with crypto" +msgstr "" + #: src/components/organisms/PostForm/PostForm.tsx:241 msgid "Write a short comment or longer project update." msgstr "" diff --git a/web-marketplace/src/lib/rdf/rdf.test.ts b/web-marketplace/src/lib/rdf/rdf.test.ts index 92532733d2..63a0f01b2b 100644 --- a/web-marketplace/src/lib/rdf/rdf.test.ts +++ b/web-marketplace/src/lib/rdf/rdf.test.ts @@ -1,6 +1,6 @@ import { getCompactedPath, validate } from './rdf'; -describe('validate', () => { +describe.skip('validate', () => { it('validate against property shapes with given group', async () => { const data = { '@context': { @@ -193,7 +193,7 @@ describe('validate', () => { }); }); -describe('getCompactedPath', () => { +describe.skip('getCompactedPath', () => { it('returns path from compacted JSON-LD', () => { const compactedPath = getCompactedPath('http://schema.org/name'); expect(compactedPath).toEqual('schema:name'); diff --git a/web-marketplace/src/storyshots.test.ts b/web-marketplace/src/storyshots.test.ts deleted file mode 100644 index f4551fcb83..0000000000 --- a/web-marketplace/src/storyshots.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import initStoryshots from '@storybook/addon-storyshots'; -import { configure, shallow } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import toJson from 'enzyme-to-json'; -import path from 'path'; - -import './jest.mock'; - -configure({ adapter: new Adapter() }); - -initStoryshots({ - configPath: path.resolve(__dirname, '../../web-storybook/.storybook'), - framework: 'react', - test: ({ story, context }) => { - const storyElement = story.render(); - const shallowTree = shallow(storyElement); - - expect(toJson(shallowTree)).toMatchSnapshot(); - }, -}); diff --git a/web-marketplace/test/setup.ts b/web-marketplace/test/setup.ts new file mode 100644 index 0000000000..bb02c60cd0 --- /dev/null +++ b/web-marketplace/test/setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/web-marketplace/test/test-utils.tsx b/web-marketplace/test/test-utils.tsx new file mode 100644 index 0000000000..180d9e66c5 --- /dev/null +++ b/web-marketplace/test/test-utils.tsx @@ -0,0 +1,107 @@ +import { ReactElement, ReactNode } from 'react'; +import { FormProvider, useForm, UseFormReturn } from 'react-hook-form'; +import { i18n } from '@lingui/core'; +import { I18nProvider } from '@lingui/react'; +import { + act, + render as rtlRender, + RenderOptions, +} from '@testing-library/react'; + +// Custom hook to setup react hook form context +export function useCustomForm( + formDefaultValues: Record, +): UseFormReturn { + return useForm({ + defaultValues: formDefaultValues, + }); +} + +// Form Provider +interface FormContextProviderProps { + children: ReactNode; + formDefaultValues: Record; +} + +const FormContextProvider = ({ + children, + formDefaultValues, +}: FormContextProviderProps) => { + const methods = useCustomForm(formDefaultValues); + + return {children}; +}; + +// Lingui Provider +interface LinguiProviderProps { + children: ReactNode; +} + +const LinguiProvider = ({ children }: LinguiProviderProps) => { + act(() => { + i18n.activate('en'); + }); + + return {children}; +}; + +// Combined Provider +interface CombinedProvidersProps { + children: ReactNode; + formDefaultValues?: Record; +} + +const CombinedProviders = ({ + children, + formDefaultValues, +}: CombinedProvidersProps) => ( + + {formDefaultValues ? ( + + {children} + + ) : ( + children + )} + +); + +// RTL Custom render function +// +// How to use the customRender function with FormContextProvider and default values +// Example usage: +// +// import { render, screen } from 'test-utils'; +// import { SomeComponent } from './SomeComponent'; +// +// test('renders SomeComponent', () => { +// render(, { +// formDefaultValues: { +// you_form_value: 'here', +// }); +// expect(screen.getByText(/something/i)).toBeInTheDocument(); +// }); +interface CustomRenderOptions extends Omit { + formDefaultValues?: Record; +} + +const customRender = (ui: ReactElement, options?: CustomRenderOptions) => { + const { formDefaultValues, ...restOptions } = options || {}; + return rtlRender(ui, { + wrapper: ({ children }) => ( + + {children} + + ), + ...restOptions, + }); +}; + +// Re-export everything +export * from '@testing-library/react'; +export * from '@testing-library/user-event'; + +// Important: +// In tests import `render` from 'test-utils' instead of '@testing-library/react'. +// Override the render method +export { customRender as render }; diff --git a/web-marketplace/tsconfig.json b/web-marketplace/tsconfig.json index adf0d02112..e27867a833 100644 --- a/web-marketplace/tsconfig.json +++ b/web-marketplace/tsconfig.json @@ -2,7 +2,14 @@ "compilerOptions": { "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], - "types": ["vite/client", "vite-plugin-svgr/client"], + "types": ["vite/client", "vite-plugin-svgr/client", "vitest/globals","@testing-library/jest-dom"], + "typeRoots": [ + "./node_modules/@types/", + "./types", + "./node_modules", + "../node_modules/@types/", + "../node_modules" + ], "allowJs": false, "skipLibCheck": true, "esModuleInterop": true, @@ -16,9 +23,13 @@ "noEmit": true, "noFallthroughCasesInSwitch": true, "jsx": "react-jsx", - "baseUrl": "./src" + "baseUrl": "./src", + "paths": { + "test/*": ["../test/*"] + } }, "include": [ + "test", "src", "types/url-search-params-polyfill.d.ts", "lib/rdf/types/rdf-validate-shacl/index.d.ts" diff --git a/web-marketplace/vite.config.mts b/web-marketplace/vite.config.mts index d38d71978f..68b9ebb85c 100644 --- a/web-marketplace/vite.config.mts +++ b/web-marketplace/vite.config.mts @@ -1,4 +1,5 @@ import NodeGlobalsPolyfillPlugin from '@esbuild-plugins/node-globals-polyfill'; +import { lingui } from '@lingui/vite-plugin'; import inject from '@rollup/plugin-inject'; import react from '@vitejs/plugin-react'; import path from 'path'; @@ -7,7 +8,6 @@ import { defineConfig } from 'vite'; import vitePluginRequire from 'vite-plugin-require'; import svgrPlugin from 'vite-plugin-svgr'; import viteTsconfigPaths from 'vite-tsconfig-paths'; -import { lingui } from '@lingui/vite-plugin'; export default defineConfig(({ mode }) => { const isDev = mode === 'development'; @@ -31,6 +31,7 @@ export default defineConfig(({ mode }) => { config: path.resolve(__dirname, 'src/config'), ledger: path.resolve(__dirname, 'src/ledger'), clients: path.resolve(__dirname, 'src/clients'), + test: path.resolve(__dirname, '../test'), }, }, build: @@ -68,5 +69,10 @@ export default defineConfig(({ mode }) => { : undefined, }, }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: './test/setup.ts', + }, }; }); diff --git a/web-www/package.json b/web-www/package.json index cb377bee7a..8aa9f32cc2 100644 --- a/web-www/package.json +++ b/web-www/package.json @@ -22,7 +22,7 @@ "@mdx-js/react": "^2.3.0", "@mui/material": "^5.10.10", "@mui/styles": "^5.10.10", - "@netlify/plugin-nextjs": "4.41.3", + "@netlify/plugin-nextjs": "5.7.0", "@next/mdx": "^13.4.19", "@types/mdx": "^2.0.10", "@types/node": "18.15.11",