diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index ca32b6703..a7b892b9b 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -19,10 +19,6 @@ on: - "test" - "staging" - "beta" - skip_build: - required: true - type: boolean - default: false resync_cardano_node_and_db: required: true type: boolean @@ -33,82 +29,8 @@ env: CARDANO_NETWORK: ${{ inputs.cardano_network || 'sanchonet' }} jobs: - check_environment_exists: - name: Check if target environment exists before proceeding - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./scripts/govtool - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Check environment exists - run: | - make check-env-defined - build_backend: - name: Build and push backend Docker image - if: ${{ ! inputs.skip_build }} - needs: - - check_environment_exists - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./scripts/govtool - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v3 - with: - aws-access-key-id: ${{ secrets.GHA_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.GHA_AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-1 - - name: Login to AWS ECR - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-region: eu-west-1 - - name: Build and push images - run: | - make docker-login - make build-backend - make push-backend - build_frontend: - name: Build and push frontend Docker image - if: ${{ ! inputs.skip_build }} - needs: - - check_environment_exists - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./scripts/govtool - steps: - - name: Checkout code - uses: actions/checkout@v3 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v3 - with: - aws-access-key-id: ${{ secrets.GHA_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.GHA_AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-1 - - name: Login to AWS ECR - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-region: eu-west-1 - - name: Build and push images - env: - GTM_ID: ${{ secrets.GTM_ID }} - SENTRY_DSN: ${{ secrets.SENTRY_DSN_FRONTEND }} - run: | - make docker-login - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - make build-frontend - make push-frontend deploy: name: Deploy app - needs: - - build_backend - - build_frontend runs-on: ubuntu-latest defaults: run: @@ -117,136 +39,58 @@ jobs: DBSYNC_POSTGRES_DB: "cexplorer" DBSYNC_POSTGRES_USER: "postgres" DBSYNC_POSTGRES_PASSWORD: "pSa8JCpQOACMUdGb" - FAKEDBSYNC_POSTGRES_DB: "govtool" - FAKEDBSYNC_POSTGRES_USER: "test" - FAKEDBSYNC_POSTGRES_PASSWORD: "test" GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} GRAFANA_SLACK_RECIPIENT: ${{ secrets.GRAFANA_SLACK_RECIPIENT }} GRAFANA_SLACK_OAUTH_TOKEN: ${{ secrets.GRAFANA_SLACK_OAUTH_TOKEN }} NGINX_BASIC_AUTH: ${{ secrets.NGINX_BASIC_AUTH }} SENTRY_DSN_BACKEND: ${{ secrets.SENTRY_DSN_BACKEND }} TRAEFIK_LE_EMAIL: "admin+govtool@binarapps.com" + GTM_ID: ${{ secrets.GTM_ID }} + SENTRY_DSN: ${{ secrets.SENTRY_DSN_FRONTEND }} + PIPELINE_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - name: Checkout code uses: actions/checkout@v3 - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v3 - with: - aws-access-key-id: ${{ secrets.GHA_AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.GHA_AWS_SECRET_ACCESS_KEY }} - aws-region: eu-west-1 - - name: Login to AWS ECR - uses: aws-actions/configure-aws-credentials@v2 - with: - aws-region: eu-west-1 - - name: Setup SSH agent - uses: webfactory/ssh-agent@v0.8.0 with: - ssh-private-key: ${{ secrets.GHA_SSH_PRIVATE_KEY }} - - name: Prepare and upload app config - run: | - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - export DOMAIN=${DOMAIN:-$ENVIRONMENT-$CARDANO_NETWORK.govtool.byron.network} - make prepare-config - make upload-config - - name: Destroy Cardano Node, DB sync and Postgres if required - if: ${{ inputs.resync_cardano_node_and_db }} - run: | - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - make destroy-cardano-node-and-dbsync; - - name: Deploy app - run: | - make docker-login - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - make deploy-stack - - name: Reprovision Grafana - run: | - sleep 30 # give grafana time to start up - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - DOMAIN=${DOMAIN:-$ENVIRONMENT-$CARDANO_NETWORK.govtool.byron.network} - curl -X POST -u "admin:$GRAFANA_ADMIN_PASSWORD" https://$DOMAIN/grafana/api/admin/provisioning/alerting/reload - curl -X POST -u "admin:$GRAFANA_ADMIN_PASSWORD" https://$DOMAIN/grafana/api/admin/provisioning/dashboards/reload - curl -X POST -u "admin:$GRAFANA_ADMIN_PASSWORD" https://$DOMAIN/grafana/api/admin/provisioning/notifications/reload - - name: Notify on Slack - env: - SLACK_WEBHOOK_URL: ${{ secrets.DEPLOY_NOTIFY_SLACK_WEBHOOK_URL }} - run: | - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - make notify - deploy_without_build: - name: Deploy app without building - if: ${{ inputs.skip_build }} - needs: - - check_environment_exists - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./scripts/govtool - env: - DBSYNC_POSTGRES_DB: "cexplorer" - DBSYNC_POSTGRES_USER: "postgres" - DBSYNC_POSTGRES_PASSWORD: "pSa8JCpQOACMUdGb" - FAKEDBSYNC_POSTGRES_DB: "govtool" - FAKEDBSYNC_POSTGRES_USER: "test" - FAKEDBSYNC_POSTGRES_PASSWORD: "test" - GRAFANA_ADMIN_PASSWORD: ${{ secrets.GRAFANA_ADMIN_PASSWORD }} - GRAFANA_SLACK_RECIPIENT: ${{ secrets.GRAFANA_SLACK_RECIPIENT }} - GRAFANA_SLACK_OAUTH_TOKEN: ${{ secrets.GRAFANA_SLACK_OAUTH_TOKEN }} - NGINX_BASIC_AUTH: ${{ secrets.NGINX_BASIC_AUTH }} - SENTRY_DSN_BACKEND: ${{ secrets.SENTRY_DSN_BACKEND }} - TRAEFIK_LE_EMAIL: "admin+govtool@binarapps.com" - steps: - - name: Checkout code - uses: actions/checkout@v3 + fetch-depth: 0 + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v3 with: aws-access-key-id: ${{ secrets.GHA_AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.GHA_AWS_SECRET_ACCESS_KEY }} aws-region: eu-west-1 + - name: Login to AWS ECR uses: aws-actions/configure-aws-credentials@v2 with: aws-region: eu-west-1 + - name: Setup SSH agent uses: webfactory/ssh-agent@v0.8.0 with: ssh-private-key: ${{ secrets.GHA_SSH_PRIVATE_KEY }} - - name: Prepare and upload app config + + - name: Set domain run: | - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - export DOMAIN=${DOMAIN:-$ENVIRONMENT-$CARDANO_NETWORK.govtool.byron.network} - make prepare-config - make upload-config + if [[ "${{ inputs.environment }}" == "staging" ]]; then + echo "DOMAIN=staging.govtool.byron.network" >> $GITHUB_ENV + elif [[ "${{ inputs.environment }}" == "beta" ]]; then + echo "DOMAIN=sanchogov.tools" >> $GITHUB_ENV + else + echo "DOMAIN=${DOMAIN:-$ENVIRONMENT-$CARDANO_NETWORK.govtool.byron.network}" >> $GITHUB_ENV + fi + - name: Destroy Cardano Node, DB sync and Postgres if required if: ${{ inputs.resync_cardano_node_and_db }} run: | - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - make destroy-cardano-node-and-dbsync; + make --debug=b destroy-cardano-node-and-dbsync + - name: Deploy app run: | - make docker-login - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - make deploy-stack + make --debug=b all + - name: Reprovision Grafana run: | sleep 30 # give grafana time to start up - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - DOMAIN=${DOMAIN:-$ENVIRONMENT-$CARDANO_NETWORK.govtool.byron.network} - curl -X POST -u "admin:$GRAFANA_ADMIN_PASSWORD" https://$DOMAIN/grafana/api/admin/provisioning/alerting/reload - curl -X POST -u "admin:$GRAFANA_ADMIN_PASSWORD" https://$DOMAIN/grafana/api/admin/provisioning/dashboards/reload - curl -X POST -u "admin:$GRAFANA_ADMIN_PASSWORD" https://$DOMAIN/grafana/api/admin/provisioning/notifications/reload - - name: Notify on Slack - run: | - if [[ "${{ inputs.environment }}" == "staging" ]]; then export DOMAIN=staging.govtool.byron.network; fi; - if [[ "${{ inputs.environment }}" == "beta" ]]; then export DOMAIN=sanchogov.tools; fi; - make notify + make --debug=b reload-grafana diff --git a/CHANGELOG.md b/CHANGELOG.md index 6303146a2..a89f944c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,17 +9,27 @@ As a minor extension, we also keep a semantic version for the `UNRELEASED` changes. ## [Unreleased] -- Change step 3 components [Issue 152](https://github.com/intersectMBO/govtool/issues/152) -- Add possibility to vote on behalf of myself - Sole Voter [Issue 119](https://github.com/IntersectMBO/govtool/issues/119) +- Add generate jsonld function [Issue 451](https://github.com/IntersectMBO/govtool/issues/451) +- Create GA review subbmision page [Issue 362](https://github.com/IntersectMBO/govtool/issues/362) +- Create GA creation form [Issue 360](https://github.com/IntersectMBO/govtool/issues/360) +- Create TextArea [Issue 110](https://github.com/IntersectMBO/govtool/issues/110) +- Choose GA type - GA Submiter [Issue 358](https://github.com/IntersectMBO/govtool/issues/358) + +- Add on-chain inputs validation [Issue 377](https://github.com/IntersectMBO/govtool/issues/377) + +### Added + +- Added `isRegisteredAsSoleVoter` and `wasRegisteredAsSoleVoter` fields to the drep/info response [Issue 212](https://github.com/IntersectMBO/govtool/issues/212) +- Abandoning registration as DRep [Issue 151](https://github.com/IntersectMBO/govtool/issues/151) +- Abandoning GA creation [Issue 359](https://github.com/IntersectMBO/govtool/issues/359) - Create DRep registration page about roles [Issue 205](https://github.com/IntersectMBO/govtool/issues/205) - Create Checkbox component. Improve Field and ControlledField [Issue 177](https://github.com/IntersectMBO/govtool/pull/177) - Vitest unit tests added for utils functions [Issue 81](https://github.com/IntersectMBO/govtool/issues/81) - i18next library added to FE [Issue 80](https://github.com/IntersectMBO/govtool/issues/80) - -### Added -- Added `isRegisteredAsSoleVoter` and `wasRegisteredAsSoleVoter` fields to the drep/info response [Issue 212](https://github.com/IntersectMBO/govtool/issues/212) +- Add possibility to vote on behalf of myself - Sole Voter [Issue 119](https://github.com/IntersectMBO/govtool/issues/119) ### Fixed + - Fix drep type detection when changing metadata [Issue 333](https://github.com/IntersectMBO/govtool/issues/333) - Fix make button disble when wallet tries connect [Issue 265](https://github.com/IntersectMBO/govtool/issues/265) - Fix drep voting power calculation [Issue 231](https://github.com/IntersectMBO/govtool/issues/231) @@ -34,6 +44,9 @@ changes. - Fixed CSP settings to allow error reports with Sentry [Issue 291](https://github.com/IntersectMBO/govtool/issues/291). ### Changed +- `drep/list` now return also `status` and `type` fields. Also it now returns the retired dreps, and you can search for given drep by name using optional query parameter. If the drep name is passed exactly, then you can even find a drep that's sole voter. [Issue 446](https://github.com/IntersectMBO/govtool/issues/446) +- `drep/list` and `drep/info` endpoints now return additional data such as metadata url and hash, and voting power [Issue 223](https://github.com/IntersectMBO/govtool/issues/223) +- `drep/info` now does not return sole voters (dreps without metadata) [Issue 317](https://github.com/IntersectMBO/govtool/issues/317) - `isRegistered` and `wasRegistered` fields in the drep/info endpoint changed to `isRegisteredAsDRep` and `wasRegisteredAsDRep` respectively [Issue 212](https://github.com/IntersectMBO/govtool/issues/212) - Update Cardano-Serialization-Lib to 12.0.0-alpha.16 [Issue 156](https://github.com/IntersectMBO/govtool/issues/156) - Changed and improved working conventions docs, PR template and codeowners file, addressing [Issue 88](https://github.com/IntersectMBO/govtool/issues/88). @@ -44,8 +57,12 @@ changes. - Adjusted Nix configuration to meet projects needs [Issue 187](https://github.com/IntersectMBO/govtool/issues/187). - Integrated OAuth to securely notify about deployment status in Slack [Issue 194](https://github.com/IntersectMBO/govtool/issues/194). - Streamlined the application build and deployment process, thereby accelerating continuous delivery (CD) and reducing the resource burden [Issue 246](https://github.com/IntersectMBO/govtool/issues/246). +- Applied unified policy on Docker images tagging [Issue 320](https://github.com/IntersectMBO/govtool/issues/320). +- Reorganised deployment Makefiles in order to better document the process and easier management [Issue 385](https://github.com/IntersectMBO/govtool/issues/385). +- Added a grafana panel to track all the deploys on the target machines [Issue 361](https://github.com/IntersectMBO/govtool/issues/361). ### Removed + - ## [sancho-v1.0.0](https://github.com/IntersectMBO/govtool/releases/tag/sancho-v1.0.0) 2023-12-17 diff --git a/README.md b/README.md index ab338a3f3..64806dc0f 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ Learn more; [docs.sanchogov.tools](https://docs.sanchogov.tools/). ## 📍 Navigation - [GovTool Backend](./govtool/backend/README.md) - [GovTool Frontend](./govtool/frontend/README.md) +- [GovTool deployment setup](./scripts/govtool/README.md) - [Documentation](./docs/) - [Tests](./tests/) diff --git a/govtool/backend/Makefile b/govtool/backend/Makefile new file mode 100644 index 000000000..1919d4847 --- /dev/null +++ b/govtool/backend/Makefile @@ -0,0 +1,31 @@ +common_mk := ../../scripts/govtool/common.mk +ifeq ($(origin $(common_mk)), undefined) + $(eval $(common_mk) := included) + include $(common_mk) +endif + +.DEFAULT_GOAL := push-backend + +# image tags +base_backend_image_tag := $(shell git hash-object $(root_dir)/govtool/backend/vva-be.cabal) +backend_image_tag := $(shell git log -n 1 --format="%H" -- $(root_dir)/govtool/backend) + +.PHONY: build-backend-base +build-backend-base: docker-login + $(call check_image_on_ecr,backend-base,$(base_backend_image_tag)) || \ + $(docker) build --file $(root_dir)/govtool/backend/Dockerfile.base --tag "$(repo_url)/backend-base:$(base_backend_image_tag)" $(root_dir)/govtool/backend + +.PHONY: push-backend-base +push-backend-base: build-backend-base + $(call check_image_on_ecr,backend-base,$(base_backend_image_tag)) || \ + $(docker) push $(repo_url)/backend-base:$(base_backend_image_tag) + +.PHONY: build-backend +build-backend: build-backend-base + $(call check_image_on_ecr,backend,$(backend_image_tag)) || \ + $(docker) build --build-arg BASE_IMAGE_TAG=$(base_backend_image_tag) --tag "$(repo_url)/backend:$(backend_image_tag)" $(root_dir)/govtool/backend + +.PHONY: push-backend +push-backend: push-backend-base build-backend + $(call check_image_on_ecr,backend,$(backend_image_tag)) || \ + $(docker) push $(repo_url)/backend:$(backend_image_tag) diff --git a/govtool/backend/default.nix b/govtool/backend/default.nix index 1a7696a4a..aab2212bc 100644 --- a/govtool/backend/default.nix +++ b/govtool/backend/default.nix @@ -1,7 +1,4 @@ -# TODO: Remove the sources file and use the nixpkgs version provided from the -# flakes lock file instead when the flakes feature is present and enabled in the -# root of the project. -{ pkgs ? (import ./sources.nix).pkgs }: +{ pkgs ? import {} }: let # This is the version of the Haskell compiler we reccommend using. ghcPackages = pkgs.haskell.packages.ghc927; diff --git a/govtool/backend/shell.nix b/govtool/backend/shell.nix new file mode 100644 index 000000000..e020a95a2 --- /dev/null +++ b/govtool/backend/shell.nix @@ -0,0 +1,16 @@ +{ pkgs ? import {} }: +let + project = import ./default.nix { inherit pkgs; }; +in +project.overrideAttrs (attrs: { + buildInputs = attrs.buildInputs ++ (with pkgs; [ + awscli + docker + git + gnumake + ]); + + shellHook = '' + ln -s ${project}/libexec/yarn-nix-example/node_modules node_modules + ''; +}) diff --git a/govtool/backend/sql/get-drep-info.sql b/govtool/backend/sql/get-drep-info.sql index de5048afb..cb50e0642 100644 --- a/govtool/backend/sql/get-drep-info.sql +++ b/govtool/backend/sql/get-drep-info.sql @@ -71,16 +71,36 @@ WasRegisteredAsSoleVoter AS ( WHERE drep_hash.raw = DRepId.raw AND drep_registration.voting_anchor_id IS NULL)) AS value +), CurrentMetadata AS ( + SELECT voting_anchor.url as url, encode(voting_anchor.data_hash, 'hex') as data_hash + FROM LatestRegistrationEntry + LEFT JOIN voting_anchor + ON voting_anchor.id = LatestRegistrationEntry.voting_anchor_id + LIMIT 1 +), CurrentVotingPower AS ( + SELECT amount as amount + FROM drep_hash + JOIN DRepId + ON drep_hash.raw = DRepId.raw + LEFT JOIN drep_distr + ON drep_distr.hash_id = drep_hash.id + ORDER BY drep_distr.epoch_no DESC + LIMIT 1 ) SELECT IsRegisteredAsDRep.value, WasRegisteredAsDRep.value, IsRegisteredAsSoleVoter.value, WasRegisteredAsSoleVoter.value, - CurrentDeposit.value + CurrentDeposit.value, + CurrentMetadata.url, + CurrentMetadata.data_hash, + CurrentVotingPower.amount FROM IsRegisteredAsDRep CROSS JOIN IsRegisteredAsSoleVoter CROSS JOIN WasRegisteredAsDRep CROSS JOIN WasRegisteredAsSoleVoter CROSS JOIN CurrentDeposit + CROSS JOIN CurrentMetadata + CROSS JOIN CurrentVotingPower diff --git a/govtool/backend/sql/list-dreps.sql b/govtool/backend/sql/list-dreps.sql index a2882b2eb..931ae6430 100644 --- a/govtool/backend/sql/list-dreps.sql +++ b/govtool/backend/sql/list-dreps.sql @@ -1,21 +1,68 @@ +WITH DRepDistr AS ( + SELECT + *, + ROW_NUMBER() OVER(PARTITION BY drep_hash.id ORDER BY drep_distr.epoch_no DESC) AS rn + FROM drep_distr + JOIN drep_hash + on drep_hash.id = drep_distr.hash_id +), DRepActivity AS ( + select + drep_activity as drep_activity, + epoch_no as epoch_no + from epoch_param + where epoch_no is not null + order by epoch_no desc + limit 1 +) + SELECT encode(dh.raw, 'hex'), + dh.view, va.url, encode(va.data_hash, 'hex'), - dr_deposit.deposit + dr_deposit.deposit, + DRepDistr.amount, + (DRepActivity.epoch_no - Max(coalesce(block.epoch_no,block_first_register.epoch_no))) <= DRepActivity.drep_activity as active, + second_to_newest_drep_registration.voting_anchor_id is not null as has_voting_anchor FROM drep_hash dh JOIN ( SELECT dr.id, dr.drep_hash_id, dr.deposit, - ROW_NUMBER() OVER(PARTITION BY dr.drep_hash_id ORDER BY dr.id DESC) AS rn + ROW_NUMBER() OVER(PARTITION BY dr.drep_hash_id ORDER BY dr.tx_id DESC) AS rn FROM drep_registration dr - where dr.deposit > 0 + where dr.deposit is not null ) as dr_deposit on dr_deposit.drep_hash_id = dh.id and dr_deposit.rn = 1 LEFT JOIN ( SELECT dr.id, dr.drep_hash_id, dr.voting_anchor_id, - ROW_NUMBER() OVER(PARTITION BY dr.drep_hash_id ORDER BY dr.id DESC) AS rn + ROW_NUMBER() OVER(PARTITION BY dr.drep_hash_id ORDER BY dr.tx_id DESC) AS rn FROM drep_registration dr ) as dr_voting_anchor on dr_voting_anchor.drep_hash_id = dh.id and dr_voting_anchor.rn = 1 -left JOIN voting_anchor va ON va.id = dr_voting_anchor.voting_anchor_id +LEFT JOIN ( + SELECT dr.id, dr.drep_hash_id, dr.voting_anchor_id, + ROW_NUMBER() OVER(PARTITION BY dr.drep_hash_id ORDER BY dr.tx_id DESC) AS rn + FROM drep_registration dr +) as second_to_newest_drep_registration +on second_to_newest_drep_registration.drep_hash_id = dh.id and second_to_newest_drep_registration.rn = 2 +LEFT JOIN DRepDistr +on DRepDistr.hash_id = dh.id and DRepDistr.rn = 1 +LEFT JOIN voting_anchor va ON va.id = dr_voting_anchor.voting_anchor_id +CROSS JOIN DRepActivity +LEFT JOIN voting_procedure as voting_procedure +on voting_procedure.drep_voter = dh.id +LEFT JOIN tx as tx +on tx.id = voting_procedure.tx_id +LEFT JOIN block as block +on block.id = tx.block_id +JOIN ( + SELECT dr.tx_id, dr.drep_hash_id, + ROW_NUMBER() OVER(PARTITION BY dr.drep_hash_id ORDER BY dr.tx_id ASC) AS rn + FROM drep_registration dr +) as dr_first_register +on dr_first_register.drep_hash_id = dh.id and dr_first_register.rn = 1 +JOIN tx as tx_first_register +on tx_first_register.id = dr_first_register.tx_id +JOIN block as block_first_register +ON block_first_register.id = tx_first_register.block_id +GROUP BY dh.raw, second_to_newest_drep_registration.voting_anchor_id, dh.view, va.url, va.data_hash, dr_deposit.deposit, DRepDistr.amount, DRepActivity.epoch_no, DRepActivity.drep_activity diff --git a/govtool/backend/src/VVA/API.hs b/govtool/backend/src/VVA/API.hs index 30557f6c6..c303fe32f 100644 --- a/govtool/backend/src/VVA/API.hs +++ b/govtool/backend/src/VVA/API.hs @@ -35,7 +35,7 @@ import VVA.Network as Network import Numeric.Natural (Natural) type VVAApi = - "drep" :> "list" :> Get '[JSON] [DRep] + "drep" :> "list" :> QueryParam "drepView" Text :> Get '[JSON] [DRep] :<|> "drep" :> "get-voting-power" :> Capture "drepId" HexText :> Get '[JSON] Integer :<|> "drep" :> "getVotes" :> Capture "drepId" HexText :> QueryParams "type" GovernanceActionType :> QueryParam "sort" GovernanceActionSortMode :> Get '[JSON] [VoteResponse] :<|> "drep" :> "info" :> Capture "drepId" HexText :> Get '[JSON] DRepInfoResponse @@ -68,11 +68,40 @@ server = drepList :<|> throw500 :<|> getNetworkMetrics -drepList :: App m => m [DRep] -drepList = do + +mapDRepType :: Types.DRepType -> DRepType +mapDRepType Types.DRep = NormalDRep +mapDRepType Types.SoleVoter = SoleVoter + +mapDRepStatus :: Types.DRepStatus -> DRepStatus +mapDRepStatus Types.Retired = Retired +mapDRepStatus Types.Active = Active +mapDRepStatus Types.Inactive = Inactive + +drepRegistrationToDrep :: Types.DRepRegistration -> DRep +drepRegistrationToDrep Types.DRepRegistration {..} = + DRep + { dRepDrepId = DRepHash dRepRegistrationDRepHash, + dRepView = dRepRegistrationView, + dRepUrl = dRepRegistrationUrl, + dRepMetadataHash = dRepRegistrationDataHash, + dRepDeposit = dRepRegistrationDeposit, + dRepVotingPower = dRepRegistrationVotingPower, + dRepStatus = mapDRepStatus dRepRegistrationStatus, + dRepType = mapDRepType dRepRegistrationType + } + +drepList :: App m => Maybe Text -> m [DRep] +drepList mDRepView = do CacheEnv {dRepListCache} <- asks vvaCache - map (\(Types.DRepRegistration drep_hash url data_hash deposit) -> DRep (DRepHash drep_hash) url data_hash deposit) - <$> cacheRequest dRepListCache () DRep.listDReps + dreps <- cacheRequest dRepListCache () DRep.listDReps + let filtered = flip filter dreps $ \Types.DRepRegistration {..} -> + case (dRepRegistrationType, mDRepView) of + (Types.SoleVoter, Just x) -> x == dRepRegistrationView + (Types.DRep, Just x) -> isInfixOf x dRepRegistrationView + (Types.DRep, Nothing) -> True + _ -> False + return $ map drepRegistrationToDrep filtered getVotingPower :: App m => HexText -> m Integer getVotingPower (unHexText -> dRepId) = do @@ -158,6 +187,9 @@ drepInfo (unHexText -> dRepId) = do , dRepInfoResponseIsRegisteredAsSoleVoter = dRepInfoIsRegisteredAsSoleVoter , dRepInfoResponseWasRegisteredAsSoleVoter = dRepInfoWasRegisteredAsSoleVoter , dRepInfoResponseDeposit = dRepInfoDeposit + , dRepInfoResponseUrl = dRepInfoUrl + , dRepInfoResponseDataHash = HexText <$> dRepInfoDataHash + , dRepInfoResponseVotingPower = dRepInfoVotingPower } getCurrentDelegation :: App m => HexText -> m (Maybe HexText) diff --git a/govtool/backend/src/VVA/API/Types.hs b/govtool/backend/src/VVA/API/Types.hs index a3c29fc57..efc79857d 100644 --- a/govtool/backend/src/VVA/API/Types.hs +++ b/govtool/backend/src/VVA/API/Types.hs @@ -380,6 +380,9 @@ data DRepInfoResponse = DRepInfoResponse , dRepInfoResponseIsRegisteredAsSoleVoter :: Bool , dRepInfoResponseWasRegisteredAsSoleVoter :: Bool , dRepInfoResponseDeposit :: Maybe Integer + , dRepInfoResponseUrl :: Maybe Text + , dRepInfoResponseDataHash :: Maybe HexText + , dRepInfoResponseVotingPower :: Maybe Integer } deriving (Generic, Show) deriveJSON (jsonOptions "dRepInfoResponse") ''DRepInfoResponse @@ -390,7 +393,10 @@ exampleDRepInfoResponse = <> "\"wasRegisteredAsDRep\": true," <> "\"isRegisteredAsSoleVoter\": true," <> "\"wasRegisteredAsSoleVoter\": true," - <> "\"deposit\": 2000000}" + <> "\"deposit\": 2000000," + <> "\"url\": \"https://drep.metadata.xyz\"," + <> "\"dataHash\": \"9af10e89979e51b8cdc827c963124a1ef4920d1253eef34a1d5cfe76438e3f11\"," + <> "\"votingPower\": 1000000}" instance ToSchema DRepInfoResponse where declareNamedSchema proxy = do @@ -505,13 +511,66 @@ instance ToSchema DRepHash where ?~ toJSON exampleDrepHash +data DRepStatus = Retired | Active | Inactive + deriving (Generic, Show) +-- ToJSON instance for DRepStatus +instance ToJSON DRepStatus where + toJSON Retired = "Retired" + toJSON Active = "Active" + toJSON Inactive = "Inactive" + +-- FromJSON instance for DRepStatus +instance FromJSON DRepStatus where + parseJSON = withText "DRepStatus" $ \case + "Retired" -> pure Retired + "Active" -> pure Active + "Inactive" -> pure Inactive + _ -> fail "Invalid DRepStatus" + +-- ToSchema instance for DRepStatus +instance ToSchema DRepStatus where + declareNamedSchema _ = pure $ NamedSchema (Just "DRepStatus") $ mempty + & type_ ?~ OpenApiString + & description ?~ "DRep Status" + & enum_ ?~ map toJSON [Retired, Active, Inactive] + + + +data DRepType = NormalDRep | SoleVoter + +instance Show DRepType where + show NormalDRep = "DRep" + show SoleVoter = "SoleVoter" + +-- ToJSON instance for DRepType +instance ToJSON DRepType where + toJSON NormalDRep = "DRep" + toJSON SoleVoter = "SoleVoter" + +-- FromJSON instance for DRepType +instance FromJSON DRepType where + parseJSON = withText "DRepType" $ \case + "DRep" -> pure NormalDRep + "SoleVoter" -> pure SoleVoter + _ -> fail "Invalid DRepType" + +-- ToSchema instance for DRepType +instance ToSchema DRepType where + declareNamedSchema _ = pure $ NamedSchema (Just "DRepType") $ mempty + & type_ ?~ OpenApiString + & description ?~ "DRep Type" + & enum_ ?~ map toJSON [NormalDRep, SoleVoter] data DRep = DRep { dRepDrepId :: DRepHash + , dRepView :: Text , dRepUrl :: Maybe Text , dRepMetadataHash :: Maybe Text , dRepDeposit :: Integer + , dRepVotingPower :: Maybe Integer + , dRepStatus :: DRepStatus + , dRepType :: DRepType } deriving (Generic, Show) @@ -520,16 +579,30 @@ deriveJSON (jsonOptions "dRep") ''DRep exampleDrep :: Text exampleDrep = "{\"drepId\": \"d3a62ffe9c214e1a6a9809f7ab2a104c117f85e1f171f8f839d94be5\"," + <> "\"view\": \"drep1l8uyy66sm8u82h82gc8hkcy2xu24dl8ffsh58aa0v7d37yp48u8\"," <> "\"url\": \"https://proposal.metadata.xyz\"," <> "\"metadataHash\": \"9af10e89979e51b8cdc827c963124a1ef4920d1253eef34a1d5cfe76438e3f11\"," - <> "\"deposit\": 0}" + <> "\"deposit\": 0," + <> "\"votingPower\": 0," + <> "\"status\": \"Active\"," + <> "\"type\": \"DRep\"}" +-- ToSchema instance for DRep instance ToSchema DRep where - declareNamedSchema _ = pure $ NamedSchema (Just "DRep") $ mempty - & type_ ?~ OpenApiObject - & description ?~ "DRep" - & example - ?~ toJSON exampleDrep + declareNamedSchema proxy = do + NamedSchema name_ schema_ <- + genericDeclareNamedSchema + ( fromAesonOptions $ jsonOptions "dRep" ) + proxy + return $ + NamedSchema name_ $ + schema_ + & description ?~ "DRep" + & example + ?~ toJSON exampleDrep + + + data GetNetworkMetricsResponse = GetNetworkMetricsResponse { getNetworkMetricsResponseCurrentTime :: UTCTime diff --git a/govtool/backend/src/VVA/DRep.hs b/govtool/backend/src/VVA/DRep.hs index 4a29872ea..211119a8e 100644 --- a/govtool/backend/src/VVA/DRep.hs +++ b/govtool/backend/src/VVA/DRep.hs @@ -31,6 +31,8 @@ import VVA.Types , Proposal(..) , Vote(..) , DRepInfo(..) + , DRepType(..) + , DRepStatus(..) ) @@ -60,7 +62,17 @@ listDReps :: m [DRepRegistration] listDReps = withPool $ \conn -> do results <- liftIO $ SQL.query_ conn listDRepsSql - return [DRepRegistration drepHash url dataHash (floor @Scientific deposit) | (drepHash, url, dataHash, deposit) <- results] + return + [ DRepRegistration drepHash drepView url dataHash (floor @Scientific deposit) votingPower status drepType + | (drepHash, drepView, url, dataHash, deposit, votingPower, isActive, wasDRep) <- results + , let status = case (isActive, deposit) of + (_, d) | d < 0 -> Retired + (isActive, d) | d >= 0 && isActive -> Active + | d >= 0 && not isActive -> Inactive + , let drepType | url == Nothing && wasDRep = DRep + | url == Nothing && not wasDRep = SoleVoter + | url /= Nothing = DRep + ] getVotesSql :: SQL.Query getVotesSql = sqlFrom $(embedFile "sql/get-votes.sql") @@ -99,12 +111,23 @@ getDRepInfo getDRepInfo drepId = withPool $ \conn -> do result <- liftIO $ SQL.query conn getDRepInfoSql (SQL.Only drepId) case result of - [(isRegisteredAsDRep, wasRegisteredAsDRep, isRegisteredAsSoleVoter, wasRegisteredAsSoleVoter, deposit)] -> + [ ( isRegisteredAsDRep + , wasRegisteredAsDRep + , isRegisteredAsSoleVoter + , wasRegisteredAsSoleVoter + , deposit + , url + , dataHash + , votingPower + )] -> return $ DRepInfo { dRepInfoIsRegisteredAsDRep = fromMaybe False isRegisteredAsDRep , dRepInfoWasRegisteredAsDRep = fromMaybe False wasRegisteredAsDRep , dRepInfoIsRegisteredAsSoleVoter = fromMaybe False isRegisteredAsSoleVoter , dRepInfoWasRegisteredAsSoleVoter = fromMaybe False wasRegisteredAsSoleVoter , dRepInfoDeposit = deposit + , dRepInfoUrl = url + , dRepInfoDataHash = dataHash + , dRepInfoVotingPower = votingPower } - [] -> return $ DRepInfo False False False False Nothing \ No newline at end of file + [] -> return $ DRepInfo False False False False Nothing Nothing Nothing Nothing \ No newline at end of file diff --git a/govtool/backend/src/VVA/Types.hs b/govtool/backend/src/VVA/Types.hs index 4d5d2e0db..e316f34e0 100644 --- a/govtool/backend/src/VVA/Types.hs +++ b/govtool/backend/src/VVA/Types.hs @@ -64,13 +64,29 @@ data DRepInfo = DRepInfo , dRepInfoIsRegisteredAsSoleVoter :: Bool , dRepInfoWasRegisteredAsSoleVoter :: Bool , dRepInfoDeposit :: Maybe Integer + , dRepInfoUrl :: Maybe Text + , dRepInfoDataHash :: Maybe Text + , dRepInfoVotingPower :: Maybe Integer } +data DRepStatus + = Retired + | Active + | Inactive + +data DRepType + = DRep + | SoleVoter + data DRepRegistration = DRepRegistration - { dRepRegistrationDrepHash :: Text + { dRepRegistrationDRepHash :: Text + , dRepRegistrationView :: Text , dRepRegistrationUrl :: Maybe Text , dRepRegistrationDataHash :: Maybe Text , dRepRegistrationDeposit :: Integer + , dRepRegistrationVotingPower :: Maybe Integer + , dRepRegistrationStatus :: DRepStatus + , dRepRegistrationType :: DRepType } data Proposal = Proposal diff --git a/govtool/frontend/Makefile b/govtool/frontend/Makefile new file mode 100644 index 000000000..2721792b7 --- /dev/null +++ b/govtool/frontend/Makefile @@ -0,0 +1,27 @@ +common_mk := ../../scripts/govtool/common.mk +ifeq ($(origin $(common_mk)), undefined) + $(eval $(common_mk) := included) + include $(common_mk) +endif + +.DEFAULT_GOAL := push-frontend + +# image tags +frontend_image_tag := $(shell git log -n 1 --format="%H" -- $(root_dir)/govtool/frontend) + +.PHONY: build-frontend +build-frontend: docker-login + @:$(call check_defined, cardano_network) + if [[ "$(cardano_network)" = "mainnet" ]]; then NETWORK_FLAG=1; else NETWORK_FLAG=0; fi; \ + $(call check_image_on_ecr,frontend,$(frontend_image_tag)) || \ + $(docker) build --tag "$(repo_url)/frontend:$(frontend_image_tag)" \ + --build-arg VITE_BASE_URL="https://$(domain)/api" \ + --build-arg VITE_GTM_ID="$${GTM_ID}" \ + --build-arg VITE_NETWORK_FLAG="$$NETWORK_FLAG" \ + --build-arg VITE_SENTRY_DSN="$${SENTRY_DSN}" \ + $(root_dir)/govtool/frontend + +.PHONY: push-frontend +push-frontend: build-frontend + $(call check_image_on_ecr,frontend,$(frontend_image_tag)) || \ + $(docker) push $(repo_url)/frontend:$(frontend_image_tag) diff --git a/govtool/frontend/default.nix b/govtool/frontend/default.nix index d9c0daef7..e96a00e56 100644 --- a/govtool/frontend/default.nix +++ b/govtool/frontend/default.nix @@ -1,17 +1,10 @@ -# This file has been generated by node2nix 1.11.1. Do not edit! - -{pkgs ? import { - inherit system; - }, system ? builtins.currentSystem, nodejs ? pkgs."nodejs_18"}: - +{ pkgs ? import {} }: let - nodeEnv = import ./node-env.nix { - inherit (pkgs) stdenv lib python2 runCommand writeTextFile writeShellScript; - inherit pkgs nodejs; - libtool = if pkgs.stdenv.isDarwin then pkgs.darwin.cctools else null; + project = pkgs.mkYarnPackage { + name = "govtool-frontend"; + src = ./.; + packageJSON = ./package.json; + yarnLock = ./yarn.lock; }; in -import ./node-packages.nix { - inherit (pkgs) fetchurl nix-gitignore stdenv lib fetchgit; - inherit nodeEnv; -} +project diff --git a/govtool/frontend/index.html b/govtool/frontend/index.html index cdf8d5d79..ac87ebc7a 100644 --- a/govtool/frontend/index.html +++ b/govtool/frontend/index.html @@ -15,6 +15,7 @@ html, body { margin: 0; + overscroll-behavior-y: none; padding: 0; } diff --git a/govtool/frontend/node-env.nix b/govtool/frontend/node-env.nix deleted file mode 100644 index bc1e36628..000000000 --- a/govtool/frontend/node-env.nix +++ /dev/null @@ -1,689 +0,0 @@ -# This file originates from node2nix - -{lib, stdenv, nodejs, python2, pkgs, libtool, runCommand, writeTextFile, writeShellScript}: - -let - # Workaround to cope with utillinux in Nixpkgs 20.09 and util-linux in Nixpkgs master - utillinux = if pkgs ? utillinux then pkgs.utillinux else pkgs.util-linux; - - python = if nodejs ? python then nodejs.python else python2; - - # Create a tar wrapper that filters all the 'Ignoring unknown extended header keyword' noise - tarWrapper = runCommand "tarWrapper" {} '' - mkdir -p $out/bin - - cat > $out/bin/tar <> $out/nix-support/hydra-build-products - ''; - }; - - # Common shell logic - installPackage = writeShellScript "install-package" '' - installPackage() { - local packageName=$1 src=$2 - - local strippedName - - local DIR=$PWD - cd $TMPDIR - - unpackFile $src - - # Make the base dir in which the target dependency resides first - mkdir -p "$(dirname "$DIR/$packageName")" - - if [ -f "$src" ] - then - # Figure out what directory has been unpacked - packageDir="$(find . -maxdepth 1 -type d | tail -1)" - - # Restore write permissions to make building work - find "$packageDir" -type d -exec chmod u+x {} \; - chmod -R u+w "$packageDir" - - # Move the extracted tarball into the output folder - mv "$packageDir" "$DIR/$packageName" - elif [ -d "$src" ] - then - # Get a stripped name (without hash) of the source directory. - # On old nixpkgs it's already set internally. - if [ -z "$strippedName" ] - then - strippedName="$(stripHash $src)" - fi - - # Restore write permissions to make building work - chmod -R u+w "$strippedName" - - # Move the extracted directory into the output folder - mv "$strippedName" "$DIR/$packageName" - fi - - # Change to the package directory to install dependencies - cd "$DIR/$packageName" - } - ''; - - # Bundle the dependencies of the package - # - # Only include dependencies if they don't exist. They may also be bundled in the package. - includeDependencies = {dependencies}: - lib.optionalString (dependencies != []) ( - '' - mkdir -p node_modules - cd node_modules - '' - + (lib.concatMapStrings (dependency: - '' - if [ ! -e "${dependency.packageName}" ]; then - ${composePackage dependency} - fi - '' - ) dependencies) - + '' - cd .. - '' - ); - - # Recursively composes the dependencies of a package - composePackage = { name, packageName, src, dependencies ? [], ... }@args: - builtins.addErrorContext "while evaluating node package '${packageName}'" '' - installPackage "${packageName}" "${src}" - ${includeDependencies { inherit dependencies; }} - cd .. - ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} - ''; - - pinpointDependencies = {dependencies, production}: - let - pinpointDependenciesFromPackageJSON = writeTextFile { - name = "pinpointDependencies.js"; - text = '' - var fs = require('fs'); - var path = require('path'); - - function resolveDependencyVersion(location, name) { - if(location == process.env['NIX_STORE']) { - return null; - } else { - var dependencyPackageJSON = path.join(location, "node_modules", name, "package.json"); - - if(fs.existsSync(dependencyPackageJSON)) { - var dependencyPackageObj = JSON.parse(fs.readFileSync(dependencyPackageJSON)); - - if(dependencyPackageObj.name == name) { - return dependencyPackageObj.version; - } - } else { - return resolveDependencyVersion(path.resolve(location, ".."), name); - } - } - } - - function replaceDependencies(dependencies) { - if(typeof dependencies == "object" && dependencies !== null) { - for(var dependency in dependencies) { - var resolvedVersion = resolveDependencyVersion(process.cwd(), dependency); - - if(resolvedVersion === null) { - process.stderr.write("WARNING: cannot pinpoint dependency: "+dependency+", context: "+process.cwd()+"\n"); - } else { - dependencies[dependency] = resolvedVersion; - } - } - } - } - - /* Read the package.json configuration */ - var packageObj = JSON.parse(fs.readFileSync('./package.json')); - - /* Pinpoint all dependencies */ - replaceDependencies(packageObj.dependencies); - if(process.argv[2] == "development") { - replaceDependencies(packageObj.devDependencies); - } - else { - packageObj.devDependencies = {}; - } - replaceDependencies(packageObj.optionalDependencies); - replaceDependencies(packageObj.peerDependencies); - - /* Write the fixed package.json file */ - fs.writeFileSync("package.json", JSON.stringify(packageObj, null, 2)); - ''; - }; - in - '' - node ${pinpointDependenciesFromPackageJSON} ${if production then "production" else "development"} - - ${lib.optionalString (dependencies != []) - '' - if [ -d node_modules ] - then - cd node_modules - ${lib.concatMapStrings (dependency: pinpointDependenciesOfPackage dependency) dependencies} - cd .. - fi - ''} - ''; - - # Recursively traverses all dependencies of a package and pinpoints all - # dependencies in the package.json file to the versions that are actually - # being used. - - pinpointDependenciesOfPackage = { packageName, dependencies ? [], production ? true, ... }@args: - '' - if [ -d "${packageName}" ] - then - cd "${packageName}" - ${pinpointDependencies { inherit dependencies production; }} - cd .. - ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} - fi - ''; - - # Extract the Node.js source code which is used to compile packages with - # native bindings - nodeSources = runCommand "node-sources" {} '' - tar --no-same-owner --no-same-permissions -xf ${nodejs.src} - mv node-* $out - ''; - - # Script that adds _integrity fields to all package.json files to prevent NPM from consulting the cache (that is empty) - addIntegrityFieldsScript = writeTextFile { - name = "addintegrityfields.js"; - text = '' - var fs = require('fs'); - var path = require('path'); - - function augmentDependencies(baseDir, dependencies) { - for(var dependencyName in dependencies) { - var dependency = dependencies[dependencyName]; - - // Open package.json and augment metadata fields - var packageJSONDir = path.join(baseDir, "node_modules", dependencyName); - var packageJSONPath = path.join(packageJSONDir, "package.json"); - - if(fs.existsSync(packageJSONPath)) { // Only augment packages that exist. Sometimes we may have production installs in which development dependencies can be ignored - console.log("Adding metadata fields to: "+packageJSONPath); - var packageObj = JSON.parse(fs.readFileSync(packageJSONPath)); - - if(dependency.integrity) { - packageObj["_integrity"] = dependency.integrity; - } else { - packageObj["_integrity"] = "sha1-000000000000000000000000000="; // When no _integrity string has been provided (e.g. by Git dependencies), add a dummy one. It does not seem to harm and it bypasses downloads. - } - - if(dependency.resolved) { - packageObj["_resolved"] = dependency.resolved; // Adopt the resolved property if one has been provided - } else { - packageObj["_resolved"] = dependency.version; // Set the resolved version to the version identifier. This prevents NPM from cloning Git repositories. - } - - if(dependency.from !== undefined) { // Adopt from property if one has been provided - packageObj["_from"] = dependency.from; - } - - fs.writeFileSync(packageJSONPath, JSON.stringify(packageObj, null, 2)); - } - - // Augment transitive dependencies - if(dependency.dependencies !== undefined) { - augmentDependencies(packageJSONDir, dependency.dependencies); - } - } - } - - if(fs.existsSync("./package-lock.json")) { - var packageLock = JSON.parse(fs.readFileSync("./package-lock.json")); - - if(![1, 2].includes(packageLock.lockfileVersion)) { - process.stderr.write("Sorry, I only understand lock file versions 1 and 2!\n"); - process.exit(1); - } - - if(packageLock.dependencies !== undefined) { - augmentDependencies(".", packageLock.dependencies); - } - } - ''; - }; - - # Reconstructs a package-lock file from the node_modules/ folder structure and package.json files with dummy sha1 hashes - reconstructPackageLock = writeTextFile { - name = "reconstructpackagelock.js"; - text = '' - var fs = require('fs'); - var path = require('path'); - - var packageObj = JSON.parse(fs.readFileSync("package.json")); - - var lockObj = { - name: packageObj.name, - version: packageObj.version, - lockfileVersion: 2, - requires: true, - packages: { - "": { - name: packageObj.name, - version: packageObj.version, - license: packageObj.license, - bin: packageObj.bin, - dependencies: packageObj.dependencies, - engines: packageObj.engines, - optionalDependencies: packageObj.optionalDependencies - } - }, - dependencies: {} - }; - - function augmentPackageJSON(filePath, packages, dependencies) { - var packageJSON = path.join(filePath, "package.json"); - if(fs.existsSync(packageJSON)) { - var packageObj = JSON.parse(fs.readFileSync(packageJSON)); - packages[filePath] = { - version: packageObj.version, - integrity: "sha1-000000000000000000000000000=", - dependencies: packageObj.dependencies, - engines: packageObj.engines, - optionalDependencies: packageObj.optionalDependencies - }; - dependencies[packageObj.name] = { - version: packageObj.version, - integrity: "sha1-000000000000000000000000000=", - dependencies: {} - }; - processDependencies(path.join(filePath, "node_modules"), packages, dependencies[packageObj.name].dependencies); - } - } - - function processDependencies(dir, packages, dependencies) { - if(fs.existsSync(dir)) { - var files = fs.readdirSync(dir); - - files.forEach(function(entry) { - var filePath = path.join(dir, entry); - var stats = fs.statSync(filePath); - - if(stats.isDirectory()) { - if(entry.substr(0, 1) == "@") { - // When we encounter a namespace folder, augment all packages belonging to the scope - var pkgFiles = fs.readdirSync(filePath); - - pkgFiles.forEach(function(entry) { - if(stats.isDirectory()) { - var pkgFilePath = path.join(filePath, entry); - augmentPackageJSON(pkgFilePath, packages, dependencies); - } - }); - } else { - augmentPackageJSON(filePath, packages, dependencies); - } - } - }); - } - } - - processDependencies("node_modules", lockObj.packages, lockObj.dependencies); - - fs.writeFileSync("package-lock.json", JSON.stringify(lockObj, null, 2)); - ''; - }; - - # Script that links bins defined in package.json to the node_modules bin directory - # NPM does not do this for top-level packages itself anymore as of v7 - linkBinsScript = writeTextFile { - name = "linkbins.js"; - text = '' - var fs = require('fs'); - var path = require('path'); - - var packageObj = JSON.parse(fs.readFileSync("package.json")); - - var nodeModules = Array(packageObj.name.split("/").length).fill("..").join(path.sep); - - if(packageObj.bin !== undefined) { - fs.mkdirSync(path.join(nodeModules, ".bin")) - - if(typeof packageObj.bin == "object") { - Object.keys(packageObj.bin).forEach(function(exe) { - if(fs.existsSync(packageObj.bin[exe])) { - console.log("linking bin '" + exe + "'"); - fs.symlinkSync( - path.join("..", packageObj.name, packageObj.bin[exe]), - path.join(nodeModules, ".bin", exe) - ); - } - else { - console.log("skipping non-existent bin '" + exe + "'"); - } - }) - } - else { - if(fs.existsSync(packageObj.bin)) { - console.log("linking bin '" + packageObj.bin + "'"); - fs.symlinkSync( - path.join("..", packageObj.name, packageObj.bin), - path.join(nodeModules, ".bin", packageObj.name.split("/").pop()) - ); - } - else { - console.log("skipping non-existent bin '" + packageObj.bin + "'"); - } - } - } - else if(packageObj.directories !== undefined && packageObj.directories.bin !== undefined) { - fs.mkdirSync(path.join(nodeModules, ".bin")) - - fs.readdirSync(packageObj.directories.bin).forEach(function(exe) { - if(fs.existsSync(path.join(packageObj.directories.bin, exe))) { - console.log("linking bin '" + exe + "'"); - fs.symlinkSync( - path.join("..", packageObj.name, packageObj.directories.bin, exe), - path.join(nodeModules, ".bin", exe) - ); - } - else { - console.log("skipping non-existent bin '" + exe + "'"); - } - }) - } - ''; - }; - - prepareAndInvokeNPM = {packageName, bypassCache, reconstructLock, npmFlags, production}: - let - forceOfflineFlag = if bypassCache then "--offline" else "--registry http://www.example.com"; - in - '' - # Pinpoint the versions of all dependencies to the ones that are actually being used - echo "pinpointing versions of dependencies..." - source $pinpointDependenciesScriptPath - - # Patch the shebangs of the bundled modules to prevent them from - # calling executables outside the Nix store as much as possible - patchShebangs . - - # Deploy the Node.js package by running npm install. Since the - # dependencies have been provided already by ourselves, it should not - # attempt to install them again, which is good, because we want to make - # it Nix's responsibility. If it needs to install any dependencies - # anyway (e.g. because the dependency parameters are - # incomplete/incorrect), it fails. - # - # The other responsibilities of NPM are kept -- version checks, build - # steps, postprocessing etc. - - export HOME=$TMPDIR - cd "${packageName}" - runHook preRebuild - - ${lib.optionalString bypassCache '' - ${lib.optionalString reconstructLock '' - if [ -f package-lock.json ] - then - echo "WARNING: Reconstruct lock option enabled, but a lock file already exists!" - echo "This will most likely result in version mismatches! We will remove the lock file and regenerate it!" - rm package-lock.json - else - echo "No package-lock.json file found, reconstructing..." - fi - - node ${reconstructPackageLock} - ''} - - node ${addIntegrityFieldsScript} - ''} - - npm ${forceOfflineFlag} --nodedir=${nodeSources} ${npmFlags} ${lib.optionalString production "--production"} rebuild - - runHook postRebuild - - if [ "''${dontNpmInstall-}" != "1" ] - then - # NPM tries to download packages even when they already exist if npm-shrinkwrap is used. - rm -f npm-shrinkwrap.json - - npm ${forceOfflineFlag} --nodedir=${nodeSources} --no-bin-links --ignore-scripts ${npmFlags} ${lib.optionalString production "--production"} install - fi - - # Link executables defined in package.json - node ${linkBinsScript} - ''; - - # Builds and composes an NPM package including all its dependencies - buildNodePackage = - { name - , packageName - , version ? null - , dependencies ? [] - , buildInputs ? [] - , production ? true - , npmFlags ? "" - , dontNpmInstall ? false - , bypassCache ? false - , reconstructLock ? false - , preRebuild ? "" - , dontStrip ? true - , unpackPhase ? "true" - , buildPhase ? "true" - , meta ? {} - , ... }@args: - - let - extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "preRebuild" "unpackPhase" "buildPhase" "meta" ]; - in - stdenv.mkDerivation ({ - name = "${name}${if version == null then "" else "-${version}"}"; - buildInputs = [ tarWrapper python nodejs ] - ++ lib.optional (stdenv.isLinux) utillinux - ++ lib.optional (stdenv.isDarwin) libtool - ++ buildInputs; - - inherit nodejs; - - inherit dontStrip; # Stripping may fail a build for some package deployments - inherit dontNpmInstall preRebuild unpackPhase buildPhase; - - compositionScript = composePackage args; - pinpointDependenciesScript = pinpointDependenciesOfPackage args; - - passAsFile = [ "compositionScript" "pinpointDependenciesScript" ]; - - installPhase = '' - source ${installPackage} - - # Create and enter a root node_modules/ folder - mkdir -p $out/lib/node_modules - cd $out/lib/node_modules - - # Compose the package and all its dependencies - source $compositionScriptPath - - ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }} - - # Create symlink to the deployed executable folder, if applicable - if [ -d "$out/lib/node_modules/.bin" ] - then - ln -s $out/lib/node_modules/.bin $out/bin - - # Fixup all executables - ls $out/bin/* | while read i - do - file="$(readlink -f "$i")" - chmod u+rwx "$file" - if isScript "$file" - then - sed -i 's/\r$//' "$file" # convert crlf to lf - fi - done - fi - - # Create symlinks to the deployed manual page folders, if applicable - if [ -d "$out/lib/node_modules/${packageName}/man" ] - then - mkdir -p $out/share - for dir in "$out/lib/node_modules/${packageName}/man/"* - do - mkdir -p $out/share/man/$(basename "$dir") - for page in "$dir"/* - do - ln -s $page $out/share/man/$(basename "$dir") - done - done - fi - - # Run post install hook, if provided - runHook postInstall - ''; - - meta = { - # default to Node.js' platforms - platforms = nodejs.meta.platforms; - } // meta; - } // extraArgs); - - # Builds a node environment (a node_modules folder and a set of binaries) - buildNodeDependencies = - { name - , packageName - , version ? null - , src - , dependencies ? [] - , buildInputs ? [] - , production ? true - , npmFlags ? "" - , dontNpmInstall ? false - , bypassCache ? false - , reconstructLock ? false - , dontStrip ? true - , unpackPhase ? "true" - , buildPhase ? "true" - , ... }@args: - - let - extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" ]; - in - stdenv.mkDerivation ({ - name = "node-dependencies-${name}${if version == null then "" else "-${version}"}"; - - buildInputs = [ tarWrapper python nodejs ] - ++ lib.optional (stdenv.isLinux) utillinux - ++ lib.optional (stdenv.isDarwin) libtool - ++ buildInputs; - - inherit dontStrip; # Stripping may fail a build for some package deployments - inherit dontNpmInstall unpackPhase buildPhase; - - includeScript = includeDependencies { inherit dependencies; }; - pinpointDependenciesScript = pinpointDependenciesOfPackage args; - - passAsFile = [ "includeScript" "pinpointDependenciesScript" ]; - - installPhase = '' - source ${installPackage} - - mkdir -p $out/${packageName} - cd $out/${packageName} - - source $includeScriptPath - - # Create fake package.json to make the npm commands work properly - cp ${src}/package.json . - chmod 644 package.json - ${lib.optionalString bypassCache '' - if [ -f ${src}/package-lock.json ] - then - cp ${src}/package-lock.json . - chmod 644 package-lock.json - fi - ''} - - # Go to the parent folder to make sure that all packages are pinpointed - cd .. - ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} - - ${prepareAndInvokeNPM { inherit packageName bypassCache reconstructLock npmFlags production; }} - - # Expose the executables that were installed - cd .. - ${lib.optionalString (builtins.substring 0 1 packageName == "@") "cd .."} - - mv ${packageName} lib - ln -s $out/lib/node_modules/.bin $out/bin - ''; - } // extraArgs); - - # Builds a development shell - buildNodeShell = - { name - , packageName - , version ? null - , src - , dependencies ? [] - , buildInputs ? [] - , production ? true - , npmFlags ? "" - , dontNpmInstall ? false - , bypassCache ? false - , reconstructLock ? false - , dontStrip ? true - , unpackPhase ? "true" - , buildPhase ? "true" - , ... }@args: - - let - nodeDependencies = buildNodeDependencies args; - extraArgs = removeAttrs args [ "name" "dependencies" "buildInputs" "dontStrip" "dontNpmInstall" "unpackPhase" "buildPhase" ]; - in - stdenv.mkDerivation ({ - name = "node-shell-${name}${if version == null then "" else "-${version}"}"; - - buildInputs = [ python nodejs ] ++ lib.optional (stdenv.isLinux) utillinux ++ buildInputs; - buildCommand = '' - mkdir -p $out/bin - cat > $out/bin/shell <=0.1.90" } }, + "node_modules/@digitalbazaar/http-client": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@digitalbazaar/http-client/-/http-client-3.4.1.tgz", + "integrity": "sha512-Ahk1N+s7urkgj7WvvUND5f8GiWEPfUw0D41hdElaqLgu8wZScI8gdI0q+qWw5N1d35x7GCRH2uk9mi+Uzo9M3g==", + "dependencies": { + "ky": "^0.33.3", + "ky-universal": "^0.11.0", + "undici": "^5.21.2" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2680,6 +2695,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@floating-ui/core": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.1.tgz", @@ -7211,6 +7234,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonld": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/@types/jsonld/-/jsonld-1.5.13.tgz", + "integrity": "sha512-n7fUU6W4kSYK8VQlf/LsE9kddBHPKhODoVOjsZswmve+2qLwBy6naWxs/EiuSZN9NU0N06Ra01FR+j87C62T0A==", + "dev": true + }, "node_modules/@types/lodash": { "version": "4.14.202", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", @@ -7869,6 +7898,17 @@ "dev": true, "license": "0BSD" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -8923,6 +8963,11 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canonicalize": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-1.0.8.tgz", + "integrity": "sha512-0CNTVCLZggSh7bc5VkX5WWPWO+cyZbNd07IHIsSXLia/eAq+r836hgk+8BKoEh7949Mda87VUOitx5OddVj64A==" + }, "node_modules/chai": { "version": "4.3.10", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.10.tgz", @@ -9546,6 +9591,14 @@ "node": ">=0.8" } }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -10677,6 +10730,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -10965,6 +11026,28 @@ "pend": "~1.2.0" } }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, "node_modules/fetch-retry": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-5.0.6.tgz", @@ -11270,6 +11353,17 @@ "node": ">= 6" } }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -13940,6 +14034,20 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonld": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/jsonld/-/jsonld-8.3.2.tgz", + "integrity": "sha512-MwBbq95szLwt8eVQ1Bcfwmgju/Y5P2GdtlHE2ncyfuYjIdEhluUVyj1eudacf1mOkWIoS9GpDBTECqhmq7EOaA==", + "dependencies": { + "@digitalbazaar/http-client": "^3.4.1", + "canonicalize": "^1.0.1", + "lru-cache": "^6.0.0", + "rdf-canonize": "^3.4.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/keen-slider": { "version": "6.8.6", "resolved": "https://registry.npmjs.org/keen-slider/-/keen-slider-6.8.6.tgz", @@ -13976,6 +14084,58 @@ "node": ">=6" } }, + "node_modules/ky": { + "version": "0.33.3", + "resolved": "https://registry.npmjs.org/ky/-/ky-0.33.3.tgz", + "integrity": "sha512-CasD9OCEQSFIam2U8efFK81Yeg8vNMTBUqtMOHlrcWQHqUX3HeCl9Dr31u4toV7emlH8Mymk5+9p0lL6mKb/Xw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky?sponsor=1" + } + }, + "node_modules/ky-universal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/ky-universal/-/ky-universal-0.11.0.tgz", + "integrity": "sha512-65KyweaWvk+uKKkCrfAf+xqN2/epw1IJDtlyCPxYffFCMR8u1sp2U65NtWpnozYfZxQ6IUzIlvUcw+hQ82U2Xw==", + "dependencies": { + "abort-controller": "^3.0.0", + "node-fetch": "^3.2.10" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/ky-universal?sponsor=1" + }, + "peerDependencies": { + "ky": ">=0.31.4", + "web-streams-polyfill": ">=3.2.1" + }, + "peerDependenciesMeta": { + "web-streams-polyfill": { + "optional": true + } + } + }, + "node_modules/ky-universal/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, "node_modules/lazy-universal-dotenv": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz", @@ -14143,7 +14303,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -14608,6 +14767,24 @@ "node": ">= 0.10.5" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -15789,6 +15966,17 @@ "node": ">= 0.8" } }, + "node_modules/rdf-canonize": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/rdf-canonize/-/rdf-canonize-3.4.0.tgz", + "integrity": "sha512-fUeWjrkOO0t1rg7B2fdyDTvngj+9RlUyL92vOdiB7c0FPguWVsniIMjEtHH+meLBO9rzkUlUzBVXgWrjI8P9LA==", + "dependencies": { + "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -16819,6 +17007,11 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -17934,6 +18127,17 @@ "node": ">=0.8.0" } }, + "node_modules/undici": { + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -19161,6 +19365,14 @@ "defaults": "^1.0.3" } }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", @@ -19459,7 +19671,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/govtool/frontend/package.json b/govtool/frontend/package.json index 5d76fd45f..b6ca5e643 100644 --- a/govtool/frontend/package.json +++ b/govtool/frontend/package.json @@ -31,6 +31,7 @@ "date-fns": "^2.30.0", "esbuild": "^0.19.8", "i18next": "^23.7.19", + "jsonld": "^8.3.2", "keen-slider": "^6.8.5", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -59,6 +60,7 @@ "@testing-library/jest-dom": "^6.1.6", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.5.2", + "@types/jsonld": "^1.5.13", "@types/node": "^20.4.8", "@types/react-dom": "^18.0.11", "@typescript-eslint/eslint-plugin": "^5.59.0", diff --git a/govtool/frontend/public/icons/Download.svg b/govtool/frontend/public/icons/Download.svg new file mode 100644 index 000000000..c39977182 --- /dev/null +++ b/govtool/frontend/public/icons/Download.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/govtool/frontend/public/icons/Link.svg b/govtool/frontend/public/icons/Link.svg new file mode 100644 index 000000000..bb50eca5b --- /dev/null +++ b/govtool/frontend/public/icons/Link.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/govtool/frontend/public/images/ProposeGovAction.png b/govtool/frontend/public/images/ProposeGovAction.png new file mode 100644 index 000000000..a355503a3 Binary files /dev/null and b/govtool/frontend/public/images/ProposeGovAction.png differ diff --git a/govtool/frontend/shell.nix b/govtool/frontend/shell.nix new file mode 100644 index 000000000..ec3697994 --- /dev/null +++ b/govtool/frontend/shell.nix @@ -0,0 +1,12 @@ +{ pkgs ? import {} }: +let + project = import ./default.nix { inherit pkgs; }; +in +project.overrideAttrs (attrs: { + buildInputs = attrs.buildInputs ++ (with pkgs; [ + awscli + docker + git + gnumake + ]); +}) diff --git a/govtool/frontend/src/App.tsx b/govtool/frontend/src/App.tsx index 13bc34a7d..3c6ead960 100644 --- a/govtool/frontend/src/App.tsx +++ b/govtool/frontend/src/App.tsx @@ -32,6 +32,7 @@ import { import { SetupInterceptors } from "./services"; import { useGetVoterInfo, useWalletConnectionListener } from "./hooks"; import { RegisterAsSoleVoter } from "./pages/RegisterAsSoleVoter"; +import { CreateGovernanceAction } from "./pages/CreateGovernanceAction"; export default function App() { const { enable, setVoter, setIsDrepLoading } = useCardano(); @@ -56,8 +57,8 @@ export default function App() { const checkTheWalletIsActive = useCallback(() => { const hrefCondition = window.location.pathname === PATHS.home || - window.location.pathname === PATHS.governance_actions || - window.location.pathname === PATHS.governance_actions_action; + window.location.pathname === PATHS.governanceActions || + window.location.pathname === PATHS.governanceActionsAction; const walletName = getItemFromLocalStorage(`${WALLET_LS_KEY}_name`); if (window.cardano) { @@ -89,32 +90,36 @@ export default function App() { } /> } > } /> } /> }> } /> } /> } /> } /> + } + /> } /> } /> >; - subtitle: string; + onChange: (newValue: string) => void; + subtitle?: string; title: string; tooltipText?: string; tooltipTitle?: string; @@ -76,13 +76,15 @@ export const ActionRadio: FC = ({ ...props }) => { )} - - {subtitle} - + {subtitle ? ( + + {subtitle} + + ) : null} ); diff --git a/govtool/frontend/src/components/atoms/FormHelpfulText.tsx b/govtool/frontend/src/components/atoms/FormHelpfulText.tsx new file mode 100644 index 000000000..4ceed950a --- /dev/null +++ b/govtool/frontend/src/components/atoms/FormHelpfulText.tsx @@ -0,0 +1,23 @@ +import { Typography } from "@mui/material"; + +import { FormHelpfulTextProps } from "./types"; + +export const FormHelpfulText = ({ + helpfulText, + helpfulTextStyle, +}: FormHelpfulTextProps) => { + return ( + helpfulText && ( + + {helpfulText} + + ) + ); +}; diff --git a/govtool/frontend/src/components/atoms/InfoText.tsx b/govtool/frontend/src/components/atoms/InfoText.tsx new file mode 100644 index 000000000..399ea6804 --- /dev/null +++ b/govtool/frontend/src/components/atoms/InfoText.tsx @@ -0,0 +1,9 @@ +import { InfoTextProps, Typography } from "."; + +export const InfoText = ({ label, sx }: InfoTextProps) => { + return ( + + {label.toLocaleUpperCase()} + + ); +}; diff --git a/govtool/frontend/src/components/atoms/Input.tsx b/govtool/frontend/src/components/atoms/Input.tsx index b29958aec..029b47a88 100644 --- a/govtool/frontend/src/components/atoms/Input.tsx +++ b/govtool/frontend/src/components/atoms/Input.tsx @@ -41,12 +41,19 @@ export const Input = forwardRef( inputProps={{ "data-testid": dataTestId }} inputRef={inputRef} sx={{ - backgroundColor: errorMessage ? "inputRed" : "transparent", + backgroundColor: errorMessage ? "inputRed" : "white", border: 1, borderColor: errorMessage ? "red" : "secondaryBlue", borderRadius: 50, padding: "8px 16px", width: "100%", + "& input.Mui-disabled": { + WebkitTextFillColor: "#4C495B", + }, + "&.Mui-disabled": { + backgroundColor: "#F5F5F8", + borderColor: "#9792B5", + }, ...sx, }} {...rest} diff --git a/govtool/frontend/src/components/atoms/ScrollToManage.tsx b/govtool/frontend/src/components/atoms/ScrollToManage.tsx index 49046d36b..1b1ff7e1a 100644 --- a/govtool/frontend/src/components/atoms/ScrollToManage.tsx +++ b/govtool/frontend/src/components/atoms/ScrollToManage.tsx @@ -25,8 +25,8 @@ export const ScrollToManage = () => { window.scrollTo(0, pathMap.get(pathname)!); } else { if ( - pathname === PATHS.dashboard_governance_actions || - pathname === PATHS.governance_actions + pathname === PATHS.dashboardGovernanceActions || + pathname === PATHS.governanceActions ) { pathMap.set(pathname, 0); } @@ -37,8 +37,8 @@ export const ScrollToManage = () => { useEffect(() => { const fn = debounce(() => { if ( - pathname === PATHS.dashboard_governance_actions || - pathname === PATHS.governance_actions + pathname === PATHS.dashboardGovernanceActions || + pathname === PATHS.governanceActions ) { pathMap.set(pathname, window.scrollY); } diff --git a/govtool/frontend/src/components/atoms/TextArea.tsx b/govtool/frontend/src/components/atoms/TextArea.tsx new file mode 100644 index 000000000..0e5b0eacd --- /dev/null +++ b/govtool/frontend/src/components/atoms/TextArea.tsx @@ -0,0 +1,71 @@ +import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"; +import { TextareaAutosize, styled } from "@mui/material"; + +import { useScreenDimension } from "@hooks"; + +import { TextAreaProps } from "./types"; + +const TextAreaBase = styled(TextareaAutosize)( + () => ` + font-family: "Poppins"; + font-size: 16px; + font-weight: 400; + ::placeholder { + font-family: "Poppins"; + font-size: 16px; + font-weight: 400; + color: #a6a6a6; + } + ` +); + +export const TextArea = forwardRef( + ({ errorMessage, maxLength = 500, onBlur, onFocus, ...props }, ref) => { + const { isMobile } = useScreenDimension(); + const textAraeRef = useRef(null); + + const handleFocus = useCallback( + (e: React.FocusEvent) => { + onFocus?.(e); + textAraeRef.current?.focus(); + }, + [] + ); + + const handleBlur = useCallback( + (e: React.FocusEvent) => { + onBlur?.(e); + textAraeRef.current?.blur(); + }, + [] + ); + + useImperativeHandle( + ref, + () => + ({ + focus: handleFocus, + blur: handleBlur, + ...textAraeRef.current, + } as unknown as HTMLTextAreaElement), + [handleBlur, handleFocus] + ); + + return ( + + ); + } +); diff --git a/govtool/frontend/src/components/atoms/index.ts b/govtool/frontend/src/components/atoms/index.ts index b022b4cea..7072b7635 100644 --- a/govtool/frontend/src/components/atoms/index.ts +++ b/govtool/frontend/src/components/atoms/index.ts @@ -6,7 +6,9 @@ export * from "./ClickOutside"; export * from "./CopyButton"; export * from "./DrawerLink"; export * from "./FormErrorMessage"; +export * from "./FormHelpfulText"; export * from "./HighlightedText"; +export * from "./InfoText"; export * from "./Input"; export * from "./Link"; export * from "./LoadingButton"; @@ -19,6 +21,7 @@ export * from "./ScrollToManage"; export * from "./ScrollToTop"; export * from "./Spacer"; export * from "./StakeRadio"; +export * from "./TextArea"; export * from "./Tooltip"; export * from "./Typography"; export * from "./VotePill"; diff --git a/govtool/frontend/src/components/atoms/types.ts b/govtool/frontend/src/components/atoms/types.ts index 8aafaaf39..34f0fdf14 100644 --- a/govtool/frontend/src/components/atoms/types.ts +++ b/govtool/frontend/src/components/atoms/types.ts @@ -4,6 +4,8 @@ import { CheckboxProps as MUICheckboxProps, InputBaseProps, TypographyProps as MUITypographyProps, + TextareaAutosizeProps, + SxProps, } from "@mui/material"; export type ButtonProps = Omit & { @@ -55,3 +57,17 @@ export type FormErrorMessageProps = { errorMessage?: string; errorStyles?: MUITypographyProps; }; + +export type FormHelpfulTextProps = { + helpfulText?: string; + helpfulTextStyle?: MUITypographyProps; +}; + +export type TextAreaProps = TextareaAutosizeProps & { + errorMessage?: string; +}; + +export type InfoTextProps = { + label: string; + sx?: SxProps; +}; diff --git a/govtool/frontend/src/components/molecules/ActionCard.tsx b/govtool/frontend/src/components/molecules/ActionCard.tsx index 3418c6471..2ac0dd63f 100644 --- a/govtool/frontend/src/components/molecules/ActionCard.tsx +++ b/govtool/frontend/src/components/molecules/ActionCard.tsx @@ -34,7 +34,6 @@ export const ActionCard: FC = ({ ...props }) => { title, } = props; const { isMobile, screenWidth } = useScreenDimension(); - const MOBILE_AND_WIDE_CONDITION = isMobile || screenWidth >= 1920; const { palette: { boxShadow2 }, @@ -42,12 +41,14 @@ export const ActionCard: FC = ({ ...props }) => { return ( {imageURL ? ( @@ -56,7 +57,7 @@ export const ActionCard: FC = ({ ...props }) => { width={imageWidth} height={imageHeight} style={{ - alignSelf: MOBILE_AND_WIDE_CONDITION ? "center" : "start", + alignSelf: screenWidth < 640 ? "center" : "start", }} /> ) : null} @@ -64,8 +65,8 @@ export const ActionCard: FC = ({ ...props }) => { @@ -78,7 +79,7 @@ export const ActionCard: FC = ({ ...props }) => { sx={{ mb: 4.25, mt: 1.75, - textAlign: MOBILE_AND_WIDE_CONDITION ? "center" : "left", + textAlign: screenWidth < 640 ? "center" : "left", }} variant={isMobile ? "body2" : "body1"} > @@ -86,16 +87,13 @@ export const ActionCard: FC = ({ ...props }) => { ) : null} - + {firstButtonLabel ? ( - + ) diff --git a/govtool/frontend/src/components/molecules/index.ts b/govtool/frontend/src/components/molecules/index.ts index 7b4c1668f..b87a802df 100644 --- a/govtool/frontend/src/components/molecules/index.ts +++ b/govtool/frontend/src/components/molecules/index.ts @@ -1,4 +1,5 @@ export * from "./ActionCard"; +export * from "./Card"; export * from "./CenteredBoxBottomButtons"; export * from "./CenteredBoxPageWrapper"; export * from "./DashboardActionCard"; @@ -10,7 +11,9 @@ export * from "./GovernanceActionCard"; export * from "./GovernanceActionsFilters"; export * from "./GovernanceActionsSorting"; export * from "./GovernanceVotedOnCard"; +export * from "./LinkWithIcon"; export * from "./OrderActionsChip"; +export * from "./Step"; export * from "./VoteActionForm"; export * from "./VotesSubmitted"; export * from "./WalletInfoCard"; diff --git a/govtool/frontend/src/components/molecules/types.ts b/govtool/frontend/src/components/molecules/types.ts new file mode 100644 index 000000000..5e817df41 --- /dev/null +++ b/govtool/frontend/src/components/molecules/types.ts @@ -0,0 +1,15 @@ +import { SxProps } from "@mui/material"; + +export type LinkWithIconProps = { + label: string; + onClick: () => void; + icon?: JSX.Element; + sx?: SxProps; +}; + +export type StepProps = { + component: JSX.Element; + label: string; + layoutStyles?: SxProps; + stepNumber: number | string; +}; diff --git a/govtool/frontend/src/components/organisms/BgCard.tsx b/govtool/frontend/src/components/organisms/BgCard.tsx index 638acd9cb..c8b7a0d48 100644 --- a/govtool/frontend/src/components/organisms/BgCard.tsx +++ b/govtool/frontend/src/components/organisms/BgCard.tsx @@ -72,37 +72,42 @@ export const BgCard = ({ return ( = 768 ? "center" : "inherit", display: "flex", flex: 1, flexDirection: "column", - marginTop: isMobile ? "97px" : "137px", + height: isMobile ? "100%" : "auto", + px: isMobile ? 0 : 5, }} > 768 ? 600 : undefined} - mb={isMobile ? undefined : 3} - pb={isMobile ? undefined : 10} - pt={isMobile ? 6 : 10} - px={isMobile ? 2 : 18.75} - sx={sx} + sx={{ + borderRadius: "20px", + boxShadow: isMobile ? "" : `2px 2px 20px 0px ${boxShadow2}`, + display: "flex", + flex: isMobile ? 1 : undefined, + flexDirection: "column", + height: "auto", + maxWidth: screenWidth > 768 ? 600 : undefined, + mb: isMobile ? undefined : 3, + pb: isMobile ? undefined : 10, + pt: isMobile ? 6 : 10, + px: isMobile ? 2 : 18.75, + width: "-webkit-fill-available", + ...sx, + }} > - + {children} {renderBackButton} {renderContinueButton} diff --git a/govtool/frontend/src/components/organisms/ControlledField/Input.tsx b/govtool/frontend/src/components/organisms/ControlledField/Input.tsx index a58d4c91d..d9a2e2f34 100644 --- a/govtool/frontend/src/components/organisms/ControlledField/Input.tsx +++ b/govtool/frontend/src/components/organisms/ControlledField/Input.tsx @@ -1,32 +1,33 @@ -import { useCallback } from "react"; +import { forwardRef, useCallback } from "react"; import { Controller, get } from "react-hook-form"; import { Field } from "@molecules"; import { ControlledInputProps, RenderInputProps } from "./types"; -export const Input = ({ - control, - name, - errors, - rules, - ...props -}: ControlledInputProps) => { - const errorMessage = get(errors, name)?.message as string; +export const Input = forwardRef( + ({ control, name, errors, rules, ...props }, ref) => { + const errorMessage = get(errors, name)?.message as string; - const renderInput = useCallback( - ({ field }: RenderInputProps) => ( - - ), - [errorMessage, props] - ); + const renderInput = useCallback( + ({ field }: RenderInputProps) => ( + + ), + [errorMessage, props] + ); - return ( - - ); -}; + return ( + + ); + } +); diff --git a/govtool/frontend/src/components/organisms/ControlledField/TextArea.tsx b/govtool/frontend/src/components/organisms/ControlledField/TextArea.tsx new file mode 100644 index 000000000..163b6562d --- /dev/null +++ b/govtool/frontend/src/components/organisms/ControlledField/TextArea.tsx @@ -0,0 +1,32 @@ +import { useCallback } from "react"; +import { Controller, get } from "react-hook-form"; + +import { Field } from "@molecules"; + +import { ControlledTextAreaProps, RenderInputProps } from "./types"; + +export const TextArea = ({ + control, + name, + errors, + rules, + ...props +}: ControlledTextAreaProps) => { + const errorMessage = get(errors, name)?.message as string; + + const renderInput = useCallback( + ({ field }: RenderInputProps) => ( + + ), + [errorMessage, props] + ); + + return ( + + ); +}; diff --git a/govtool/frontend/src/components/organisms/ControlledField/index.tsx b/govtool/frontend/src/components/organisms/ControlledField/index.tsx index 1cb8c7d5c..e7a50f7ed 100644 --- a/govtool/frontend/src/components/organisms/ControlledField/index.tsx +++ b/govtool/frontend/src/components/organisms/ControlledField/index.tsx @@ -2,10 +2,12 @@ import React, { PropsWithChildren } from "react"; import { Checkbox } from "./Checkbox"; import { Input } from "./Input"; +import { TextArea } from "./TextArea"; type ControlledFieldComposition = React.FC & { Checkbox: typeof Checkbox; Input: typeof Input; + TextArea: typeof TextArea; }; const ControlledField: ControlledFieldComposition = ({ children }) => { @@ -14,5 +16,6 @@ const ControlledField: ControlledFieldComposition = ({ children }) => { ControlledField.Checkbox = Checkbox; ControlledField.Input = Input; +ControlledField.TextArea = TextArea; export { ControlledField }; diff --git a/govtool/frontend/src/components/organisms/ControlledField/types.ts b/govtool/frontend/src/components/organisms/ControlledField/types.ts index f75c9b978..6bf85727d 100644 --- a/govtool/frontend/src/components/organisms/ControlledField/types.ts +++ b/govtool/frontend/src/components/organisms/ControlledField/types.ts @@ -1,4 +1,8 @@ -import { CheckboxFieldProps, InputFieldProps } from "@molecules"; +import { + CheckboxFieldProps, + InputFieldProps, + TextAreaFieldProps, +} from "@molecules"; import { Control, ControllerRenderProps, @@ -9,8 +13,8 @@ import { } from "react-hook-form"; export type ControlledInputProps = InputFieldProps & { - control: Control; - errors: FieldErrors; + control?: Control; + errors?: FieldErrors; name: Path; rules?: Omit; }; @@ -28,3 +32,10 @@ export type ControlledCheckboxProps = Omit< export type RenderInputProps = { field: ControllerRenderProps; }; + +export type ControlledTextAreaProps = TextAreaFieldProps & { + control: Control; + errors: FieldErrors; + name: Path; + rules?: Omit; +}; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/ChooseGovernanceActionType.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/ChooseGovernanceActionType.tsx new file mode 100644 index 000000000..e90606635 --- /dev/null +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/ChooseGovernanceActionType.tsx @@ -0,0 +1,71 @@ +import { Dispatch, SetStateAction } from "react"; +import { ActionRadio, Spacer, Typography } from "@atoms"; +import { + useCreateGovernanceActionForm, + useScreenDimension, + useTranslation, +} from "@hooks"; +import { GovernanceActionType } from "@/types/governanceAction"; + +import { BgCard } from "../BgCard"; + +type ChooseGovernanceActionTypeProps = { + setStep: Dispatch>; +}; + +export const ChooseGovernanceActionType = ({ + setStep, +}: ChooseGovernanceActionTypeProps) => { + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + const { getValues, setValue, watch } = useCreateGovernanceActionForm(); + + const isContinueButtonDisabled = !watch("governance_action_type"); + + const onClickContinue = () => { + setStep(3); + }; + + const onClickBack = () => { + setStep(1); + }; + + // TODO: Add tooltips when they will be available + const renderGovernanceActionTypes = () => + Object.keys(GovernanceActionType).map( + (type, index, governanceActionTypes) => { + const isChecked = getValues("governance_action_type") === type; + return ( +
+ + {index + 1 < governanceActionTypes.length ? : null} +
+ ); + } + ); + + const onChangeType = (value: string) => { + setValue("governance_action_type", value as GovernanceActionType); + }; + + return ( + + + {t("createGovernanceAction.chooseGATypeTitle")} + + + {renderGovernanceActionTypes()} + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx new file mode 100644 index 000000000..0d49018e5 --- /dev/null +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/CreateGovernanceActionForm.tsx @@ -0,0 +1,168 @@ +import { Dispatch, SetStateAction, useCallback } from "react"; +import { useFieldArray } from "react-hook-form"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; + +import { Button, InfoText, Spacer, Typography } from "@atoms"; +import { GOVERNANCE_ACTION_FIELDS } from "@consts"; +import { useCreateGovernanceActionForm, useTranslation } from "@hooks"; +import { Field } from "@molecules"; + +import { BgCard } from "../BgCard"; +import { ControlledField } from "../ControlledField"; +import { GovernanceActionField } from "@/types/governanceAction"; +import { URL_REGEX } from "@/utils"; + +const LINK_PLACEHOLDER = "https://website.com/"; +const MAX_NUMBER_OF_LINKS = 8; + +type CreateGovernanceActionFormProps = { + setStep: Dispatch>; +}; + +export const CreateGovernanceActionForm = ({ + setStep, +}: CreateGovernanceActionFormProps) => { + const { t } = useTranslation(); + const { control, errors, getValues, register, reset, watch } = + useCreateGovernanceActionForm(); + + const isError = Object.keys(errors).length > 0; + + const type = getValues("governance_action_type"); + const { + append, + fields: links, + remove, + } = useFieldArray({ + control, + name: "links", + }); + + // TODO: Replace any + const isContinueButtonDisabled = + Object.keys(GOVERNANCE_ACTION_FIELDS[type!]).some( + (field: any) => !watch(field) + ) || isError; + + const onClickContinue = () => { + setStep(4); + }; + + const onClickBack = () => { + reset(); + setStep(2); + }; + + const renderGovernanceActionField = () => { + return Object.entries(GOVERNANCE_ACTION_FIELDS[type!]).map( + ([key, field]) => { + const fieldProps = { + helpfulText: field.tipI18nKey ? t(field.tipI18nKey) : undefined, + key, + label: t(field.labelI18nKey), + layoutStyles: { mb: 3 }, + name: key, + placeholder: field.placeholderI18nKey + ? t(field.placeholderI18nKey) + : undefined, + rules: field.rules, + }; + + if (field.component === GovernanceActionField.Input) { + return ( + + ); + } + if (field.component === GovernanceActionField.TextArea) { + return ( + + ); + } + } + ); + }; + + const addLink = useCallback(() => { + append({ link: "" }); + }, [append]); + + const removeLink = useCallback( + (index: number) => { + remove(index); + }, + [remove] + ); + + const renderLinks = useCallback(() => { + return links.map((field, index) => { + return ( + 1 ? ( + removeLink(index)} + /> + ) : null + } + key={field.id} + label={t("forms.link") + ` ${index + 1}`} + layoutStyles={{ mb: 3 }} + placeholder={LINK_PLACEHOLDER} + name={`links.${index}.link`} + rules={{ + required: { + value: true, + message: t("createGovernanceAction.fields.validations.required"), + }, + pattern: { + value: URL_REGEX, + message: t("createGovernanceAction.fields.validations.url"), + }, + }} + /> + ); + }); + }, [links]); + + return ( + + + + {t("createGovernanceAction.formTitle")} + + + + + {renderGovernanceActionField()} + + + {t("createGovernanceAction.references")} + + + {renderLinks()} + {links?.length < MAX_NUMBER_OF_LINKS ? ( + + ) : null} + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/ReviewCreatedGovernanceAction.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/ReviewCreatedGovernanceAction.tsx new file mode 100644 index 000000000..6afc22249 --- /dev/null +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/ReviewCreatedGovernanceAction.tsx @@ -0,0 +1,125 @@ +import { Box } from "@mui/material"; +import DriveFileRenameOutlineOutlinedIcon from "@mui/icons-material/DriveFileRenameOutlineOutlined"; + +import { Button, Spacer, Typography } from "@atoms"; +import { ICONS } from "@consts"; +import { + defaulCreateGovernanceActionValues, + useCreateGovernanceActionForm, + useTranslation, +} from "@hooks"; +import { LinkWithIcon } from "@molecules"; +import { openInNewTab } from "@utils"; + +import { BgCard } from "../BgCard"; +import { Dispatch, SetStateAction } from "react"; + +type ReviewCreatedGovernanceActionProps = { + setStep: Dispatch>; +}; + +export const ReviewCreatedGovernanceAction = ({ + setStep, +}: ReviewCreatedGovernanceActionProps) => { + const { t } = useTranslation(); + const { getValues } = useCreateGovernanceActionForm(); + const values = getValues(); + + const onClickContinue = () => { + setStep(5); + }; + + const onClickBackButton = () => { + setStep(3); + }; + + const onClickEditSubmission = () => { + setStep(3); + }; + + const onClickLink = (link: string) => { + openInNewTab(link); + }; + + const renderReviewFields = () => { + return Object.entries(values) + .filter( + ([key]) => + !Object.keys(defaulCreateGovernanceActionValues).includes(key) || + key === "governance_action_type" + ) + .map(([key, value]) => { + const label = + key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "); + + return ( + + + {label} + + + {value as string} + + + ); + }); + }; + + const renderLinks = () => { + const links = values["links"]?.map((item) => item.link) ?? []; + const areLinks = links.some((item) => item); + + return areLinks ? ( + <> + + {t("createGovernanceAction.supportingLinks")} + + {links.map((link: string) => { + return link ? ( + } + label={link} + onClick={() => onClickLink(link)} + sx={{ mb: 1.75 }} + /> + ) : null; + })} + + ) : null; + }; + + return ( + + + {t("createGovernanceAction.reviewSubmission")} + + + + + {renderReviewFields()} + {renderLinks()} + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StorageInformation.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StorageInformation.tsx new file mode 100644 index 000000000..dd1840868 --- /dev/null +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StorageInformation.tsx @@ -0,0 +1,134 @@ +import { Dispatch, SetStateAction, useCallback, useState } from "react"; +import { Box } from "@mui/material"; +import OpenInNewIcon from "@mui/icons-material/OpenInNew"; + +import { Button, Spacer, Typography } from "@atoms"; +import { ICONS } from "@consts"; +import { useCreateGovernanceActionForm, useTranslation } from "@hooks"; +import { Step } from "@molecules"; +import { BgCard, ControlledField } from "@organisms"; +import { URL_REGEX, downloadJson, openInNewTab } from "@utils"; + +type StorageInformationProps = { + setStep: Dispatch>; +}; + +export const StorageInformation = ({ setStep }: StorageInformationProps) => { + const { t } = useTranslation(); + const { + control, + errors, + createGovernanceAction, + generateJsonBody, + getValues, + watch, + } = useCreateGovernanceActionForm(); + const [isJsonDownloaded, setIsJsonDownloaded] = useState(false); + + // TODO: change on correct file name + const fileName = getValues("governance_action_type"); + + // TODO: Change link to correct + const openGuideAboutStoringInformation = useCallback( + () => openInNewTab("https://sancho.network/"), + [] + ); + + const isActionButtonDisabled = !watch("storingURL") || !isJsonDownloaded; + + const onClickBack = useCallback(() => setStep(5), []); + + const onClickDownloadJson = async () => { + const data = getValues(); + const jsonBody = await generateJsonBody(data); + downloadJson(jsonBody, fileName); + setIsJsonDownloaded(true); + }; + + return ( + + + {t("createGovernanceAction.storingInformationTitle")} + + + {t("createGovernanceAction.storingInformationDescription")} + + + } + sx={{ width: "fit-content" }} + > + {`${fileName}.jsonld`} + + } + label={t("createGovernanceAction.storingInformationStep1Label")} + stepNumber={1} + /> + + + } + onClick={openGuideAboutStoringInformation} + size="extraLarge" + sx={{ width: "fit-content" }} + variant="text" + > + {t("createGovernanceAction.storingInformationStep2Link")} + + } + label={t("createGovernanceAction.storingInformationStep2Label")} + stepNumber={2} + /> + + + } + label={t("createGovernanceAction.storingInformationStep3Label")} + stepNumber={3} + /> + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StoreDataInfo.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StoreDataInfo.tsx new file mode 100644 index 000000000..a36888444 --- /dev/null +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/StoreDataInfo.tsx @@ -0,0 +1,70 @@ +import { Dispatch, SetStateAction } from "react"; +import { Box, Link } from "@mui/material"; + +import { Spacer, Typography } from "@atoms"; +import { + useCreateGovernanceActionForm, + useScreenDimension, + useTranslation, +} from "@hooks"; +import { BgCard, ControlledField } from "@organisms"; +import { openInNewTab } from "@utils"; + +type StoreDataInfoProps = { + setStep: Dispatch>; +}; + +export const StoreDataInfo = ({ setStep }: StoreDataInfoProps) => { + const { t } = useTranslation(); + const { control, errors, watch } = useCreateGovernanceActionForm(); + const { isMobile } = useScreenDimension(); + + // TODO: change link when available + const openLink = () => { + openInNewTab("https://docs.sanchogov.tools"); + }; + + const isContinueDisabled = !watch("storeData"); + + const onClickContinue = () => { + setStep(6); + }; + + const onClickBack = () => { + setStep(4); + }; + + return ( + + + {t("createGovernanceAction.storeDataTitle")} + + + {t("createGovernanceAction.storeDataLink")} + + + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/WhatGovernanceActionIsAbout.tsx b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/WhatGovernanceActionIsAbout.tsx new file mode 100644 index 000000000..380b3c8ab --- /dev/null +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/WhatGovernanceActionIsAbout.tsx @@ -0,0 +1,59 @@ +import { useCallback, Dispatch, SetStateAction } from "react"; +import { Trans } from "react-i18next"; + +import { Typography } from "@atoms"; +import { useScreenDimension, useTranslation } from "@hooks"; +import { + correctAdaFormat, + getItemFromLocalStorage, + PROTOCOL_PARAMS_KEY, +} from "@utils"; + +import { BgCard } from ".."; + +type WhatGovernanceActionIsAboutProps = { + onClickCancel: () => void; + setStep: Dispatch>; +}; + +export const WhatGovernanceActionIsAbout = ({ + onClickCancel, + setStep, +}: WhatGovernanceActionIsAboutProps) => { + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + + const deposit = getItemFromLocalStorage(PROTOCOL_PARAMS_KEY); + + const onClickContinue = useCallback(() => setStep(2), []); + + return ( + + + {t("createGovernanceAction.creatingAGovernanceAction")} + + + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/index.ts b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/index.ts new file mode 100644 index 000000000..caa32e87c --- /dev/null +++ b/govtool/frontend/src/components/organisms/CreateGovernanceActionSteps/index.ts @@ -0,0 +1,6 @@ +export * from "./ChooseGovernanceActionType"; +export * from "./CreateGovernanceActionForm"; +export * from "./ReviewCreatedGovernanceAction"; +export * from "./StorageInformation"; +export * from "./StoreDataInfo"; +export * from "./WhatGovernanceActionIsAbout"; diff --git a/govtool/frontend/src/components/organisms/DashboardCards.tsx b/govtool/frontend/src/components/organisms/DashboardCards.tsx index 73208b3cb..6f2d1c11d 100644 --- a/govtool/frontend/src/components/organisms/DashboardCards.tsx +++ b/govtool/frontend/src/components/organisms/DashboardCards.tsx @@ -32,7 +32,7 @@ export const DashboardCards = () => { const navigate = useNavigate(); const { currentDelegation, isCurrentDelegationLoading } = useGetAdaHolderCurrentDelegationQuery(stakeKey); - const { screenWidth } = useScreenDimension(); + const { isMobile, screenWidth } = useScreenDimension(); const { openModal } = useModal(); const [isRetirementLoading, setIsRetirementLoading] = useState(false); @@ -306,22 +306,31 @@ export const DashboardCards = () => { return isDrepLoading ? ( ) : ( 1728 + ? "repeat(3, minmax(300px, 572px))" + : "repeat(2, minmax(300px, 572px))", + px: isMobile ? 2 : 5, + py: 3, + rowGap: 3, + }} > {/* DELEGATION CARD */} { "https://docs.sanchogov.tools/faqs/what-does-it-mean-to-register-as-a-drep" ) } - secondButtonVariant={"outlined"} + secondButtonVariant="outlined" imageURL={IMAGES.soleVoterImage} /> {/* REGISTARTION AS SOLE VOTER CARD END*/} @@ -502,7 +511,7 @@ export const DashboardCards = () => { navigate(PATHS.dashboard_governance_actions)} + firstButtonAction={() => navigate(PATHS.dashboardGovernanceActions)} firstButtonLabel={t( `dashboard.govActions.${ voter?.isRegisteredAsDRep ? "reviewAndVote" : "view" @@ -512,6 +521,31 @@ export const DashboardCards = () => { title={t("dashboard.govActions.title")} /> {/* GOV ACTIONS LIST CARD END*/} + {/* GOV ACTIONS LIST CARD */} + navigate(PATHS.createGovernanceAction)} + firstButtonLabel={t( + `dashboard.proposeGovernanceAction.${ + // TODO: add isPendingGovernanceAction to the context + // isPendingGovernanceAction ? "propose" : "viewGovernanceActions" + `propose` + }` + )} + secondButtonLabel={t("learnMore")} + secondButtonAction={() => + openInNewTab( + "https://docs.sanchogov.tools/faqs/what-is-a-governance-action" + ) + } + secondButtonVariant="outlined" + imageURL={IMAGES.proposeGovActionImage} + title={t("dashboard.proposeGovernanceAction.title")} + /> + {/* GOV ACTIONS LIST CARD END*/} ); }; diff --git a/govtool/frontend/src/components/organisms/DashboardDrawerMobile.tsx b/govtool/frontend/src/components/organisms/DashboardDrawerMobile.tsx new file mode 100644 index 000000000..05a821cdf --- /dev/null +++ b/govtool/frontend/src/components/organisms/DashboardDrawerMobile.tsx @@ -0,0 +1,93 @@ +import { Box, Grid, IconButton, SwipeableDrawer } from "@mui/material"; + +import { Background, Link } from "@atoms"; +import { CONNECTED_NAV_ITEMS, ICONS } from "@consts"; +import { useCardano } from "@context"; +import { DRepInfoCard, WalletInfoCard } from "@molecules"; +import { useScreenDimension } from "@hooks"; +import { openInNewTab } from "@utils"; + +import { DashboardDrawerMobileProps } from "./types"; + +const DRAWER_PADDING = 2; +// 8 is number of multiple in Material UI 2 is left and right side +const CALCULATED_DRAWER_PADDING = DRAWER_PADDING * 8 * 2; + +export const DashboardDrawerMobile = ({ + isDrawerOpen, + setIsDrawerOpen, +}: DashboardDrawerMobileProps) => { + const { screenWidth } = useScreenDimension(); + const { voter } = useCardano(); + + const openDrawer = () => { + setIsDrawerOpen(true); + }; + + const closeDrawer = () => { + setIsDrawerOpen(false); + }; + + return ( + + + + + + + + + + + + {CONNECTED_NAV_ITEMS.map((navItem) => ( + + { + navItem.newTabLink && openInNewTab(navItem.newTabLink); + setIsDrawerOpen(false); + }} + isConnectWallet + /> + + ))} + + + {(voter?.isRegisteredAsDRep || voter?.isRegisteredAsSoleVoter) && ( + + )} + + + + + + ); +}; diff --git a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx index 413f76818..815b1f37f 100644 --- a/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx +++ b/govtool/frontend/src/components/organisms/DashboardGovernanceActionDetails.tsx @@ -46,7 +46,7 @@ export const DashboardGovernanceActionDetails = () => { const breadcrumbs = [ @@ -87,10 +87,10 @@ export const DashboardGovernanceActionDetails = () => { onClick={() => navigate( state && state.openedFromCategoryPage - ? generatePath(PATHS.dashboard_governance_actions_category, { + ? generatePath(PATHS.dashboardGovernanceActionsCategory, { category: state.type, }) - : PATHS.dashboard_governance_actions, + : PATHS.dashboardGovernanceActions, { state: { isVotedListOnLoad: state && state.vote ? true : false, diff --git a/govtool/frontend/src/components/organisms/DashboardTopNav.tsx b/govtool/frontend/src/components/organisms/DashboardTopNav.tsx index 00f44fedd..bcfb5baaa 100644 --- a/govtool/frontend/src/components/organisms/DashboardTopNav.tsx +++ b/govtool/frontend/src/components/organisms/DashboardTopNav.tsx @@ -1,181 +1,97 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { Box, Grid, IconButton, SwipeableDrawer } from "@mui/material"; +import { useEffect, useState } from "react"; +import { Box, IconButton } from "@mui/material"; -import { Background, Link, VotingPowerChips, Typography } from "@atoms"; -import { useScreenDimension, useTranslation } from "@hooks"; -import { ICONS, PATHS } from "@consts"; -import { useCardano } from "@context"; -import { DRepInfoCard, WalletInfoCard } from "@molecules"; -import { openInNewTab } from "@utils"; +import { VotingPowerChips, Typography } from "@atoms"; +import { ICONS } from "@consts"; +import { useScreenDimension } from "@hooks"; +import { DashboardDrawerMobile } from "@organisms"; type DashboardTopNavProps = { - imageSRC?: string; - imageWidth?: number; - imageHeight?: number; title: string; - isDrawer?: boolean; isVotingPowerHidden?: boolean; }; -const DRAWER_PADDING = 2; -const CALCULATED_DRAWER_PADDING = DRAWER_PADDING * 8 * 2; +const POSITION_TO_BLUR = 50; export const DashboardTopNav = ({ title, - imageSRC, - imageWidth, - imageHeight, isVotingPowerHidden, }: DashboardTopNavProps) => { - const { isMobile, screenWidth } = useScreenDimension(); - const { voter } = useCardano(); - const navigate = useNavigate(); + const [windowScroll, setWindowScroll] = useState(0); + const { isMobile } = useScreenDimension(); const [isDrawerOpen, setIsDrawerOpen] = useState(false); - const { t } = useTranslation(); + + const openDrawer = () => { + setIsDrawerOpen(true); + }; + + useEffect(() => { + const onScroll = () => { + setWindowScroll(window.scrollY); + }; + + window.addEventListener("scroll", onScroll, { + passive: true, + }); + + return () => window.removeEventListener("scroll", onScroll); + }, []); return ( - - - {imageSRC ? ( - { - navigate(PATHS.dashboard); - }} - > - - - ) : null} - {!isMobile && title ? ( - {title} - ) : null} - - - {!isVotingPowerHidden && } + <> + POSITION_TO_BLUR + ? "rgba(256, 256, 256, 0.7)" + : isMobile + ? "#FBFBFF59" + : "transparent", + borderBottom: "1px solid #D6E2FF", + display: "flex", + justifyContent: "space-between", + position: "sticky", + px: isMobile ? 2 : 5, + py: 3, + top: 0, + width: "fill-available", + zIndex: 100, + }} + > + + {isMobile ? ( + + ) : null} + {!isMobile && title ? ( + {title} + ) : null} + + + {!isVotingPowerHidden && } + {isMobile && ( + + + + )} + {isMobile && ( - setIsDrawerOpen(true)} - > - - + )} - {isMobile && ( - setIsDrawerOpen(false)} - onOpen={() => setIsDrawerOpen(true)} - > - - - - - - setIsDrawerOpen(false)} - > - - - - - - { - setIsDrawerOpen(false); - }} - isConnectWallet - /> - - - { - setIsDrawerOpen(false); - }} - isConnectWallet - /> - - - { - openInNewTab( - "https://docs.sanchogov.tools/about/what-is-sanchonet-govtool" - ); - setIsDrawerOpen(false); - }} - isConnectWallet - /> - - - { - openInNewTab("https://docs.sanchogov.tools/faqs"); - setIsDrawerOpen(false); - }} - isConnectWallet - /> - - - - {(voter?.isRegisteredAsDRep || - voter?.isRegisteredAsSoleVoter) && } - - - - - - )} - + {isMobile && title ? ( + + {title} + + ) : null} + ); }; diff --git a/govtool/frontend/src/components/organisms/Drawer.tsx b/govtool/frontend/src/components/organisms/Drawer.tsx index 914674bda..4eb123f5e 100644 --- a/govtool/frontend/src/components/organisms/Drawer.tsx +++ b/govtool/frontend/src/components/organisms/Drawer.tsx @@ -4,84 +4,81 @@ import { NavLink } from "react-router-dom"; import { DrawerLink, Typography } from "@atoms"; import { CONNECTED_NAV_ITEMS, ICONS, IMAGES, PATHS } from "@consts"; import { useCardano } from "@context"; -import { WalletInfoCard, DRepInfoCard } from "@molecules"; -import { openInNewTab } from "@/utils"; import { useTranslation } from "@hooks"; +import { WalletInfoCard, DRepInfoCard } from "@molecules"; +import { openInNewTab } from "@utils"; export const Drawer = () => { const { voter } = useCardano(); const { t } = useTranslation(); return ( - - + + + + - - - - - {CONNECTED_NAV_ITEMS.map((navItem) => ( - - openInNewTab(navItem.newTabLink) - : undefined - } - /> - - ))} - - - {voter?.isRegisteredAsDRep && } - - - + {CONNECTED_NAV_ITEMS.map((navItem) => ( + - openInNewTab( - "https://docs.sanchogov.tools/support/get-help-in-discord" - ) + {...navItem} + onClick={ + navItem.newTabLink + ? () => openInNewTab(navItem.newTabLink) + : undefined } /> - - - {t("footer.copyright")} - + + ))} + + + {voter?.isRegisteredAsDRep && } + + + + + openInNewTab( + "https://docs.sanchogov.tools/support/get-help-in-discord" + ) + } + /> + + {t("footer.copyright")} + ); diff --git a/govtool/frontend/src/components/organisms/DrawerMobile.tsx b/govtool/frontend/src/components/organisms/DrawerMobile.tsx index 9357c396a..d5606fe94 100644 --- a/govtool/frontend/src/components/organisms/DrawerMobile.tsx +++ b/govtool/frontend/src/components/organisms/DrawerMobile.tsx @@ -1,17 +1,12 @@ -import { Dispatch, SetStateAction } from "react"; import { Box, Grid, IconButton, SwipeableDrawer } from "@mui/material"; -import { Background, Button, Link } from "../atoms"; +import { Background, Button, Link, Typography } from "@atoms"; import { ICONS, IMAGES, NAV_ITEMS } from "@consts"; import { useScreenDimension, useTranslation } from "@hooks"; import { useModal } from "@context"; import { openInNewTab } from "@utils"; -type DrawerMobileProps = { - isConnectButton: boolean; - isDrawerOpen: boolean; - setIsDrawerOpen: Dispatch>; -}; +import { DrawerMobileProps } from "./types"; const DRAWER_PADDING = 2; const CALCULATED_DRAWER_PADDING = DRAWER_PADDING * 8 * 2; @@ -25,6 +20,9 @@ export const DrawerMobile = ({ const { openModal } = useModal(); const { t } = useTranslation(); + const onClickHelp = () => + openInNewTab("https://docs.sanchogov.tools/support/get-help-in-discord"); + return ( - + ) : null} - - {NAV_ITEMS.map((navItem) => ( - - { - if (navItem.newTabLink) openInNewTab(navItem.newTabLink); - setIsDrawerOpen(false); - }} - size="big" - /> - - ))} - + + + {NAV_ITEMS.map((navItem) => ( + + { + if (navItem.newTabLink) openInNewTab(navItem.newTabLink); + setIsDrawerOpen(false); + }} + size="big" + /> + + ))} + + + + + + + {t("menu.help")} + diff --git a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx index 914e09861..c457062c5 100644 --- a/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx +++ b/govtool/frontend/src/components/organisms/GovernanceActionsToVote.tsx @@ -6,7 +6,6 @@ import { Slider } from "./Slider"; import { Typography } from "@atoms"; import { - useGetDRepVotesQuery, useGetProposalsQuery, useScreenDimension, useTranslation, @@ -114,7 +113,7 @@ export const GovernanceActionsToVote = ({ : navigate( onDashboard ? generatePath( - PATHS.dashboard_governance_actions_action, + PATHS.dashboardGovernanceActionsAction, { proposalId: getFullGovActionId( item.txHash, @@ -122,7 +121,7 @@ export const GovernanceActionsToVote = ({ ), } ) - : PATHS.governance_actions_action.replace( + : PATHS.governanceActionsAction.replace( ":proposalId", getFullGovActionId( item.txHash, diff --git a/govtool/frontend/src/components/organisms/Hero.tsx b/govtool/frontend/src/components/organisms/Hero.tsx index 497f230e0..bdc6bcbbe 100644 --- a/govtool/frontend/src/components/organisms/Hero.tsx +++ b/govtool/frontend/src/components/organisms/Hero.tsx @@ -13,18 +13,9 @@ export const Hero = () => { const { isEnabled } = useCardano(); const { openModal } = useModal(); const navigate = useNavigate(); - const { isMobile, screenWidth, pagePadding } = useScreenDimension(); + const { isMobile, screenWidth } = useScreenDimension(); const { t } = useTranslation(); - const IMAGE_SIZE = - screenWidth < 768 - ? 140 - : screenWidth < 1024 - ? 400 - : screenWidth < 1440 - ? 500 - : screenWidth < 1920 - ? 600 - : 720; + const IMAGE_SIZE = screenWidth < 640 ? 300 : screenWidth < 860 ? 400 : 600; const onClickVotingPower = useCallback( () => @@ -37,15 +28,15 @@ export const Hero = () => { alignItems="center" display="flex" flex={1} + marginTop={16} flexDirection="row" - height={screenWidth < 1024 ? "70vh" : "75vh"} overflow="visible" position="relative" - px={pagePadding} + px={screenWidth < 640 ? 3 : screenWidth < 1512 ? 10 : 14} > @@ -56,16 +47,12 @@ export const Hero = () => { sx={{ maxWidth: 630, my: 4, - ...(isMobile ? {} : { whiteSpace: "pre-line" }), + whiteSpace: "pre-line", }} - variant={isMobile ? "body2" : "title2"} + variant="title2" > { = 1728 + ? IMAGE_SIZE / 8 + : screenWidth >= 1512 + ? -(IMAGE_SIZE / 12) + : screenWidth >= 860 + ? -(IMAGE_SIZE / 8) + : -(IMAGE_SIZE / 4) + } + top={-80} zIndex={-1} > diff --git a/govtool/frontend/src/components/organisms/HomeCards.tsx b/govtool/frontend/src/components/organisms/HomeCards.tsx index d8ca35a92..4539587c1 100644 --- a/govtool/frontend/src/components/organisms/HomeCards.tsx +++ b/govtool/frontend/src/components/organisms/HomeCards.tsx @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { useNavigate } from "react-router-dom"; import { Box } from "@mui/material"; @@ -6,12 +7,11 @@ import { useModal } from "@context"; import { ActionCard } from "@molecules"; import { useScreenDimension, useTranslation } from "@hooks"; import { openInNewTab } from "@utils"; -import { useCallback } from "react"; export const HomeCards = () => { const navigate = useNavigate(); const { openModal } = useModal(); - const { isMobile, screenWidth } = useScreenDimension(); + const { screenWidth } = useScreenDimension(); const { t } = useTranslation(); const openWalletModal = useCallback(() => { @@ -35,110 +35,114 @@ export const HomeCards = () => { ); const onClickLearnMoreAboutSoleVoterRegistration = useCallback( - () => openInNewTab("https://www.google.com"), + // TODO: Update the link + () => openInNewTab("https://docs.sanchogov.tools/"), + [] + ); + + const onClickLearnMoreAboutProposingGovAction = useCallback( + // TODO: Update the link + () => openInNewTab("https://docs.sanchogov.tools/"), [] ); const navigateToGovActions = useCallback( - () => navigate(PATHS.governance_actions), + () => navigate(PATHS.governanceActions), [navigate] ); return ( = 1920 ? "1fr 1fr 1fr" : "1fr"} - mb={6} - mt={screenWidth < 1024 ? 6 : 14} + gridTemplateColumns={screenWidth >= 1920 ? "1fr 1fr" : "1fr"} + justifyItems="center" + mb={screenWidth < 640 ? 10 : 17} + mt={screenWidth < 640 ? 10 : 14.5} px={ - screenWidth < 768 + screenWidth < 640 ? 2 : screenWidth < 1024 - ? 12 + ? 5 : screenWidth < 1440 - ? 24 + ? 10 : 34 } - rowGap={6} + rowGap={5} > - - {/* DELEGATE CARD */} - - {/* DELEGATE CARD END*/} - - - {/* REGISTER AS DREP CARD */} - - {/* REGISTER AS DREP CARD END */} - - - {/* REGISTER AS SOLE VOTER CARD */} - - {/* REGISTER AS SOLE VOTER CARD END */} - - - {/* GOV ACTIONS CARD */} - - {/* GOV ACTIONS CARD */} - + {/* DELEGATE CARD */} + + {/* DELEGATE CARD END*/} + {/* REGISTER AS DREP CARD */} + + {/* REGISTER AS DREP CARD END */} + {/* REGISTER AS SOLE VOTER CARD */} + + {/* REGISTER AS SOLE VOTER CARD END */} + {/* GOV ACTIONS CARD */} + + {/* GOV ACTIONS CARD END*/} + {/* PROPOSE GOV ACTION CARD */} + + {/* PROPOSE GOV ACTION CARD END*/} ); }; diff --git a/govtool/frontend/src/components/organisms/RegisterAsdRepStepOne.tsx b/govtool/frontend/src/components/organisms/RegisterAsdRepStepOne.tsx index 3a68ce015..67702466e 100644 --- a/govtool/frontend/src/components/organisms/RegisterAsdRepStepOne.tsx +++ b/govtool/frontend/src/components/organisms/RegisterAsdRepStepOne.tsx @@ -14,8 +14,10 @@ import { import { BgCard } from "."; export const RegisterAsdRepStepOne = ({ + onClickCancel, setStep, }: { + onClickCancel: () => void; setStep: Dispatch>; }) => { const { t } = useTranslation(); @@ -25,7 +27,7 @@ export const RegisterAsdRepStepOne = ({ const onClickContinue = useCallback(() => setStep(2), []); - const openLearMoreAboutDrep = useCallback( + const openLearnMoreAboutDrep = useCallback( () => openInNewTab("https://sancho.network/roles/drep"), [] ); @@ -35,6 +37,7 @@ export const RegisterAsdRepStepOne = ({ actionButtonLabel={t("continue")} backButtonLabel={t("cancel")} onClickActionButton={onClickContinue} + onClickBackButton={onClickCancel} sx={{ paddingBottom: isMobile ? undefined : 3 }} > @@ -54,7 +57,7 @@ export const RegisterAsdRepStepOne = ({ components={[ , ]} diff --git a/govtool/frontend/src/components/organisms/Slider.tsx b/govtool/frontend/src/components/organisms/Slider.tsx index e6024a772..07bf2cd90 100644 --- a/govtool/frontend/src/components/organisms/Slider.tsx +++ b/govtool/frontend/src/components/organisms/Slider.tsx @@ -105,12 +105,12 @@ export const Slider = ({ onClick={() => onDashboard ? navigate( - generatePath(PATHS.dashboard_governance_actions_category, { + generatePath(PATHS.dashboardGovernanceActionsCategory, { category: navigateKey, }) ) : navigate( - generatePath(PATHS.governance_actions_category, { + generatePath(PATHS.governanceActionsCategory, { category: navigateKey, }) ) @@ -157,15 +157,12 @@ export const Slider = ({ onClick={() => onDashboard ? navigate( - generatePath( - PATHS.dashboard_governance_actions_category, - { - category: navigateKey, - } - ) + generatePath(PATHS.dashboardGovernanceActionsCategory, { + category: navigateKey, + }) ) : navigate( - generatePath(PATHS.governance_actions_category, { + generatePath(PATHS.governanceActionsCategory, { category: navigateKey, }) ) diff --git a/govtool/frontend/src/components/organisms/TopNav.tsx b/govtool/frontend/src/components/organisms/TopNav.tsx index 229ba6526..e77932536 100644 --- a/govtool/frontend/src/components/organisms/TopNav.tsx +++ b/govtool/frontend/src/components/organisms/TopNav.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from "react"; import { NavLink, useNavigate } from "react-router-dom"; import { AppBar, Box, Grid, IconButton } from "@mui/material"; +import MenuIcon from "@mui/icons-material/Menu"; import { Button, Link } from "@atoms"; import { ICONS, IMAGES, PATHS, NAV_ITEMS } from "@consts"; @@ -34,6 +35,10 @@ export const TopNav = ({ isConnectButton = true }) => { return () => window.removeEventListener("scroll", onScroll); }, []); + const openDrawer = () => { + setIsDrawerOpen(true); + }; + return ( { borderBottom: isMobile ? 1 : 0, borderColor: "lightblue", boxShadow: 0, + justifyContent: "center", flex: 1, flexDirection: "row", - justifyContent: "space-between", position: "fixed", - px: screenWidth >= 1920 ? 37 : isMobile ? 2 : 5, + px: isMobile ? 2 : 5, py: 3, }} > - (isConnectButton ? {} : disconnectWallet())} - to={PATHS.home} + - - - {!isMobile ? ( - - ) : ( - <> - - {isConnectButton ? ( - - ) : null} - setIsDrawerOpen(true)} - sx={{ padding: 0 }} - > - - - - - - )} + ) : null} + {screenWidth >= 768 ? ( + + + + ) : ( + + )} + + + + )} + ); diff --git a/govtool/frontend/src/components/organisms/index.ts b/govtool/frontend/src/components/organisms/index.ts index 5387a425c..57b806873 100644 --- a/govtool/frontend/src/components/organisms/index.ts +++ b/govtool/frontend/src/components/organisms/index.ts @@ -2,8 +2,10 @@ export * from "./BgCard"; export * from "./ChooseStakeKeyPanel"; export * from "./ChooseWalletModal"; export * from "./ControlledField"; +export * from "./CreateGovernanceActionSteps"; export * from "./DashboardCards"; export * from "./DashboardCards"; +export * from "./DashboardDrawerMobile"; export * from "./DashboardGovernanceActionDetails"; export * from "./DashboardGovernanceActions"; export * from "./DashboardGovernanceActionsVotedOn"; @@ -21,11 +23,11 @@ export * from "./HomeCards"; export * from "./RegisterAsdRepStepOne"; export * from "./RegisterAsdRepStepThree"; export * from "./RegisterAsdRepStepTwo"; -export * from "./Slider"; -export * from "./RegisterAsSoleVoterBoxContent"; export * from "./RegisterAsSoleVoterBox"; -export * from "./RetireAsSoleVoterBoxContent"; +export * from "./RegisterAsSoleVoterBoxContent"; export * from "./RetireAsSoleVoterBox"; +export * from "./RetireAsSoleVoterBoxContent"; +export * from "./Slider"; export * from "./StatusModal"; export * from "./TopNav"; export * from "./VotingPowerModal"; diff --git a/govtool/frontend/src/components/organisms/types.ts b/govtool/frontend/src/components/organisms/types.ts index 0eaad74af..4207dd2db 100644 --- a/govtool/frontend/src/components/organisms/types.ts +++ b/govtool/frontend/src/components/organisms/types.ts @@ -1,12 +1,22 @@ import { SxProps } from "@mui/material"; +import { Dispatch, SetStateAction } from "react"; export type BgCardProps = { actionButtonLabel: string; backButtonLabel?: string; children: React.ReactNode; isLoadingActionButton?: boolean; - isActionButtonDisabled?:boolean; + isActionButtonDisabled?: boolean; onClickBackButton?: () => void; onClickActionButton: () => void; sx?: SxProps; }; + +export type DashboardDrawerMobileProps = { + isDrawerOpen: boolean; + setIsDrawerOpen: Dispatch>; +}; + +export type DrawerMobileProps = DashboardDrawerMobileProps & { + isConnectButton: boolean; +}; diff --git a/govtool/frontend/src/consts/colors.ts b/govtool/frontend/src/consts/colors.ts new file mode 100644 index 000000000..94ce6b2c1 --- /dev/null +++ b/govtool/frontend/src/consts/colors.ts @@ -0,0 +1,107 @@ +type ColorKey = 'c50' | 'c100' | 'c200' | 'c300' | 'c400' | 'c500' | 'c600' | 'c700' | 'c800' | 'c900'; + +type ColorType = Record; + +export const primaryBlue: ColorType = { + c50: '#E6EBF7', + c100: '#D6E2FF', + c200: '#99ADDE', + c300: '#6685CE', + c400: '#335CBD', + c500: '#0033AD', + c600: '#002682', + c700: '#001A57', + c800: '#000D2B', + c900: '#000511', +}; + +export const orange: ColorType = { + c50: '#FFF0E7', + c100: '#FFE0CE', + c200: '#FFC19D', + c300: '#FFA26C', + c400: '#FF833B', + c500: '#FF640A', + c600: '#BF4B08', + c700: '#803205', + c800: '#401903', + c900: '#1A0A01', +}; + +export const cyan: ColorType = { + c50: '#E9F5F8', + c100: '#D2EAF0', + c200: '#A4D4E0', + c300: '#77BFD1', + c400: '#49A9C1', + c500: '#1C94B2', + c600: '#156F86', + c700: '#0E4A59', + c800: '#07252D', + c900: '#030F12', +}; + +export const fadedPurple: ColorType = { + c50: '#F5F5F8', + c100: '#EAE9F0', + c200: '#D5D3E1', + c300: '#C1BED3', + c400: '#ACA8C4', + c500: '#9792B5', + c600: '#716E88', + c700: '#4C495B', + c800: '#26252D', + c900: '#0F0F12', +}; + +export const gray: ColorType = { + c50: '#F4F4F4', + c100: '#E8E9E8', + c200: '#D2D3D2', + c300: '#A5A6A5', + c400: '#8E908E', + c500: '#6B6C6B', + c600: '#474847', + c700: '#242424', + c800: '#0E0E0E', + c900: '#000000', +}; + +export const successGreen: ColorType = { + c50: '#F0F9EE', + c100: '#E0F2DC', + c200: '#C0E4BA', + c300: '#A1D797', + c400: '#81C975', + c500: '#62BC52', + c600: '#4A8D3E', + c700: '#315E29', + c800: '#192F15', + c900: '#0A1308', +}; + +export const progressYellow: ColorType = { + c50: '#FCF6EA', + c100: '#F8ECD4', + c200: '#F2D9A9', + c300: '#EBC67F', + c400: '#E5B354', + c500: '#DEA029', + c600: '#A7781F', + c700: '#6F5015', + c800: '#38280A', + c900: '#161004', +}; + +export const errorRed: ColorType = { + c50: '#FBEBEB', + c100: '#F6D5D5', + c200: '#EDACAC', + c300: '#E58282', + c400: '#DC5959', + c500: '#D32F2F', + c600: '#9E2323', + c700: '#6A1818', + c800: '#350C0C', + c900: '#150505', +}; diff --git a/govtool/frontend/src/consts/governanceActionFields.ts b/govtool/frontend/src/consts/governanceActionFields.ts new file mode 100644 index 000000000..e5663758b --- /dev/null +++ b/govtool/frontend/src/consts/governanceActionFields.ts @@ -0,0 +1,192 @@ +import I18n from "@/i18n"; +import { + GovernanceActionType, + GovernanceActionFields, + GovernanceActionField, + SharedGovernanceActionFieldSchema, +} from "@/types/governanceAction"; +import { bech32 } from "bech32"; + +export const CIP_100 = + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#"; +export const CIP_108 = + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#"; + +const sharedGovernanceActionFields: SharedGovernanceActionFieldSchema = { + title: { + component: GovernanceActionField.Input, + labelI18nKey: "createGovernanceAction.fields.declarations.title.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.title.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + }, + }, + abstract: { + component: GovernanceActionField.TextArea, + labelI18nKey: "createGovernanceAction.fields.declarations.abstract.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.abstract.placeholder", + tipI18nKey: "createGovernanceAction.fields.declarations.abstract.tip", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + maxLength: { + value: 500, + message: I18n.t("createGovernanceAction.fields.validations.maxLength", { + maxLength: 500, + }), + }, + }, + }, + motivation: { + component: GovernanceActionField.TextArea, + labelI18nKey: "createGovernanceAction.fields.declarations.motivation.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.motivation.placeholder", + tipI18nKey: "createGovernanceAction.fields.declarations.motivation.tip", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + maxLength: { + value: 500, + message: I18n.t("createGovernanceAction.fields.validations.maxLength", { + maxLength: 500, + }), + }, + }, + }, + rationale: { + component: GovernanceActionField.TextArea, + labelI18nKey: "createGovernanceAction.fields.declarations.rationale.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.rationale.placeholder", + tipI18nKey: "createGovernanceAction.fields.declarations.rationale.tip", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + maxLength: { + value: 500, + message: I18n.t("createGovernanceAction.fields.validations.maxLength", { + maxLength: 500, + }), + }, + }, + }, +}; + +export const GOVERNANCE_ACTION_FIELDS: GovernanceActionFields = { + [GovernanceActionType.Info]: sharedGovernanceActionFields, + [GovernanceActionType.Treasury]: { + ...sharedGovernanceActionFields, + receivingAddress: { + component: GovernanceActionField.Input, + labelI18nKey: + "createGovernanceAction.fields.declarations.receivingAddress.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.receivingAddress.placeholder", + rules: { + validate: (value) => { + if (bech32.decode(value).words.length) { + return true; + } else { + return I18n.t("createGovernanceAction.fields.validations.bech32"); + } + }, + }, + }, + amount: { + component: GovernanceActionField.Input, + labelI18nKey: "createGovernanceAction.fields.declarations.amount.label", + placeholderI18nKey: + "createGovernanceAction.fields.declarations.amount.placeholder", + rules: { + required: { + value: true, + message: I18n.t("createGovernanceAction.fields.validations.required"), + }, + validate: (value) => { + if (Number.isInteger(Number(value))) { + return true; + } else { + return I18n.t("createGovernanceAction.fields.validations.number"); + } + }, + }, + }, + }, +} as const; + +const commonContext = { + "@language": "en-us", + CIP100: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0100/README.md#", + CIP108: + "https://github.com/cardano-foundation/CIPs/blob/master/CIP-0108/README.md#", + hashAlgorithm: "CIP100:hashAlgorithm", + body: { + "@id": "CIP108:body", + "@context": { + references: { + "@id": "CIP108:references", + "@container": "@set" as "@set", + "@context": { + GovernanceMetadata: "CIP100:GovernanceMetadataReference", + Other: "CIP100:OtherReference", + label: "CIP100:reference-label", + uri: "CIP100:reference-uri", + referenceHash: { + "@id": "CIP108:referenceHash", + "@context": { + hashDigest: "CIP108:hashDigest", + hashAlgorithm: "CIP100:hashAlgorithm", + }, + }, + }, + }, + title: "CIP108:title", + abstract: "CIP108:abstract", + motivation: "CIP108:motivation", + rationale: "CIP108:rationale", + }, + }, + authors: { + "@id": "CIP100:authors", + "@container": "@set" as "@set", + "@context": { + name: "http://xmlns.com/foaf/0.1/name", + witness: { + "@id": "CIP100:witness", + "@context": { + witnessAlgorithm: "CIP100:witnessAlgorithm", + publicKey: "CIP100:publicKey", + signature: "CIP100:signature", + }, + }, + }, + }, +}; + +export const GOVERNANCE_ACTION_CONTEXTS = { + [GovernanceActionType.Info]: commonContext, + [GovernanceActionType.Treasury]: { + ...commonContext, + body: { + ...commonContext.body, + "@context": { + ...commonContext.body["@context"], + amount: "CIP108:amount", + receivingAddress: "CIP108:receivingAddress", + }, + }, + }, +}; diff --git a/govtool/frontend/src/consts/icons.ts b/govtool/frontend/src/consts/icons.ts index 93574c43b..2a5fc394d 100644 --- a/govtool/frontend/src/consts/icons.ts +++ b/govtool/frontend/src/consts/icons.ts @@ -1,8 +1,8 @@ export const ICONS = { appLogoIcon: "/icons/AppLogo.svg", arrowDownIcon: "/icons/ArrowDown.svg", - arrowRightIcon: "/icons/ArrowRight.svg", arrowLeftThinIcon: "/icons/ArrowLeftThin.svg", + arrowRightIcon: "/icons/ArrowRight.svg", checkCircleIcon: "/icons/CheckCircle.svg", closeDrawerIcon: "/icons/CloseIcon.svg", closeIcon: "/icons/Close.svg", @@ -12,6 +12,7 @@ export const ICONS = { copyWhiteIcon: "/icons/CopyWhite.svg", dashboardActiveIcon: "/icons/DashboardActive.svg", dashboardIcon: "/icons/Dashboard.svg", + download: "/icons/Download.svg", drawerIcon: "/icons/DrawerIcon.svg", externalLinkIcon: "/icons/ExternalLink.svg", faqsActiveIcon: "/icons/FaqsActive.svg", @@ -23,6 +24,7 @@ export const ICONS = { guidesActiveIcon: "/icons/GuidesActive.svg", guidesIcon: "/icons/Guides.svg", helpIcon: "/icons/Help.svg", + link: "/icons/Link.svg", sortActiveIcon: "/icons/SortActive.svg", sortIcon: "/icons/Sort.svg", sortWhiteIcon: "/icons/SortWhite.svg", diff --git a/govtool/frontend/src/consts/images.ts b/govtool/frontend/src/consts/images.ts index 72f60559d..7f97c824a 100644 --- a/govtool/frontend/src/consts/images.ts +++ b/govtool/frontend/src/consts/images.ts @@ -4,12 +4,13 @@ export const IMAGES = { bgBlue: "/images/BGBlue.png", bgOrange: "/images/BGOrange.png", errorPageImage: "/images/ErrorPageImage.png", - heroImage: "/images/HeroImage.png", - soleVoterImage: "/images/GovActionsSoleVoter.png", govActionDefaultImage: "/images/GovActionDefault.png", govActionDelegateImage: "/images/GovActionDelegate.png", govActionListImage: "/images/GovActionList.png", govActionRegisterImage: "/images/GovActionRegister.png", + heroImage: "/images/HeroImage.png", + proposeGovActionImage: "/images/ProposeGovAction.png", + soleVoterImage: "/images/GovActionsSoleVoter.png", successImage: "/images/Success.png", warningImage: "/images/Warning.png", warningYellowImage: "/images/WarningYellow.png", diff --git a/govtool/frontend/src/consts/index.ts b/govtool/frontend/src/consts/index.ts index 65168f9c6..3b8fcac7f 100644 --- a/govtool/frontend/src/consts/index.ts +++ b/govtool/frontend/src/consts/index.ts @@ -1,3 +1,5 @@ +export * from "./colors"; +export * from "./governanceActionFields"; export * from "./governanceActionsFilters"; export * from "./governanceActionsSorting"; export * from "./icons"; diff --git a/govtool/frontend/src/consts/navItems.ts b/govtool/frontend/src/consts/navItems.ts index ddaf508e5..1c3d0ec37 100644 --- a/govtool/frontend/src/consts/navItems.ts +++ b/govtool/frontend/src/consts/navItems.ts @@ -3,14 +3,14 @@ import { PATHS } from "./paths"; export const NAV_ITEMS = [ { - dataTestId: "home-link", + dataTestId: "dashboard-link", navTo: PATHS.home, - label: "Home", + label: "Dashboard", newTabLink: null, }, { dataTestId: "governance-actions-link", - navTo: PATHS.governance_actions, + navTo: PATHS.governanceActions, label: "Governance Actions", newTabLink: null, }, @@ -40,7 +40,7 @@ export const CONNECTED_NAV_ITEMS = [ { dataTestId: "governance-actions-link", label: "Governance Actions", - navTo: PATHS.dashboard_governance_actions, + navTo: PATHS.dashboardGovernanceActions, activeIcon: ICONS.governanceActionsActiveIcon, icon: ICONS.governanceActionsIcon, newTabLink: null, diff --git a/govtool/frontend/src/consts/paths.ts b/govtool/frontend/src/consts/paths.ts index 4ac0cbde2..679962a36 100644 --- a/govtool/frontend/src/consts/paths.ts +++ b/govtool/frontend/src/consts/paths.ts @@ -1,18 +1,18 @@ export const PATHS = { - dashboard_governance_actions_action: - "/connected/governance_actions/:proposalId", - dashboard_governance_actions_category: + createGovernanceAction: "/create_governance_action", + dashboardGovernanceActionsAction: "/connected/governance_actions/:proposalId", + dashboardGovernanceActionsCategory: "/connected/governance_actions/category/:category", - dashboard_governance_actions: "/connected/governance_actions", + dashboardGovernanceActions: "/connected/governance_actions", dashboard: "/dashboard", delegateTodRep: "/delegate", error: "/error", faqs: "/faqs", - governance_actions_action: "/governance_actions/:proposalId", - governance_actions_category_action: + governanceActions: "/governance_actions", + governanceActionsAction: "/governance_actions/:proposalId", + governanceActionsCategoryAction: "/governance_actions/category/:category/:proposalId", - governance_actions_category: "/governance_actions/category/:category", - governance_actions: "/governance_actions", + governanceActionsCategory: "/governance_actions/category/:category", guides: "/guides", home: "/", registerAsdRep: "/register", diff --git a/govtool/frontend/src/context/index.tsx b/govtool/frontend/src/context/contextProviders.tsx similarity index 100% rename from govtool/frontend/src/context/index.tsx rename to govtool/frontend/src/context/contextProviders.tsx diff --git a/govtool/frontend/src/context/index.ts b/govtool/frontend/src/context/index.ts new file mode 100644 index 000000000..fec76656f --- /dev/null +++ b/govtool/frontend/src/context/index.ts @@ -0,0 +1,4 @@ +export * from "./contextProviders"; +export * from "./modal"; +export * from "./snackbar"; +export * from "./wallet"; diff --git a/govtool/frontend/src/context/wallet.tsx b/govtool/frontend/src/context/wallet.tsx index 1b29b1d5d..2d72d4659 100644 --- a/govtool/frontend/src/context/wallet.tsx +++ b/govtool/frontend/src/context/wallet.tsx @@ -10,8 +10,6 @@ import { } from "react"; import { Address, - Anchor, - AnchorDataHash, BigNum, Certificate, CertificatesBuilder, @@ -33,13 +31,18 @@ import { TransactionUnspentOutput, TransactionUnspentOutputs, TransactionWitnessSet, - URL, Value, VoteDelegation, Voter, VotingBuilder, VotingProcedure, StakeRegistration, + VotingProposalBuilder, + InfoAction, + VotingProposal, + GovernanceAction, + TreasuryWithdrawals, + TreasuryWithdrawalsAction, } from "@emurgo/cardano-serialization-lib-asmjs"; import { Buffer } from "buffer"; import { useNavigate } from "react-router-dom"; @@ -53,20 +56,21 @@ import { PATHS } from "@consts"; import { CardanoApiWallet, VoterInfo, Protocol } from "@models"; import type { StatusModalState } from "@organisms"; import { - getPubDRepID, - WALLET_LS_KEY, - DELEGATE_TRANSACTION_KEY, - REGISTER_TRANSACTION_KEY, + checkIsMaintenanceOn, DELEGATE_TO_KEY, - PROTOCOL_PARAMS_KEY, + DELEGATE_TRANSACTION_KEY, + generateAnchor, getItemFromLocalStorage, - setItemToLocalStorage, - removeItemFromLocalStorage, + getPubDRepID, openInNewTab, + PROTOCOL_PARAMS_KEY, + REGISTER_SOLE_VOTER_TRANSACTION_KEY, + REGISTER_TRANSACTION_KEY, + removeItemFromLocalStorage, SANCHO_INFO_KEY, + setItemToLocalStorage, VOTE_TRANSACTION_KEY, - checkIsMaintenanceOn, - REGISTER_SOLE_VOTER_TRANSACTION_KEY, + WALLET_LS_KEY, } from "@utils"; import { getEpochParams, getTransactionStatus } from "@services"; import { @@ -94,6 +98,18 @@ type TransactionHistoryItem = { export type DRepActionType = "retirement" | "registration" | "update" | ""; +type InfoProps = { + hash: string; + url: string; +}; + +type TreasuryProps = { + amount: string; + hash: string; + receivingAddress: string; + url: string; +}; + interface CardanoContext { address?: string; disconnectWallet: () => Promise; @@ -152,6 +168,12 @@ interface CardanoContext { isPendingTransaction: () => boolean; isDrepLoading: boolean; setIsDrepLoading: Dispatch>; + buildNewInfoGovernanceAction: ( + infoProps: InfoProps + ) => Promise; + buildTreasuryGovernanceAction: ( + treasuryProps: TreasuryProps + ) => Promise; } type Utxos = { @@ -212,9 +234,9 @@ function CardanoProvider(props: Props) { { proposalId: string } & TransactionHistoryItem >({ time: undefined, transactionHash: "", proposalId: "" }); const [isDrepLoading, setIsDrepLoading] = useState(true); - const { addSuccessAlert, addWarningAlert, addErrorAlert } = useSnackbar(); const { t } = useTranslation(); + const epochParams = getItemFromLocalStorage(PROTOCOL_PARAMS_KEY); const isPendingTransaction = useCallback(() => { if ( @@ -1061,7 +1083,6 @@ function CardanoProvider(props: Props) { cip95MetadataHash?: string ): Promise => { try { - const epochParams = getItemFromLocalStorage(PROTOCOL_PARAMS_KEY); // Build DRep Registration Certificate const certBuilder = CertificatesBuilder.new(); @@ -1072,9 +1093,7 @@ function CardanoProvider(props: Props) { let dRepRegCert; // If there is an anchor if (cip95MetadataURL && cip95MetadataHash) { - const url = URL.new(cip95MetadataURL); - const hash = AnchorDataHash.from_hex(cip95MetadataHash); - const anchor = Anchor.new(url, hash); + const anchor = generateAnchor(cip95MetadataURL, cip95MetadataHash); // Create cert object using one Ada as the deposit dRepRegCert = DrepRegistration.new_with_anchor( dRepCred, @@ -1097,7 +1116,7 @@ function CardanoProvider(props: Props) { throw e; } }, - [dRepID] + [epochParams, dRepID] ); // conway alpha @@ -1117,9 +1136,7 @@ function CardanoProvider(props: Props) { let dRepUpdateCert; // If there is an anchor if (cip95MetadataURL && cip95MetadataHash) { - const url = URL.new(cip95MetadataURL); - const hash = AnchorDataHash.from_hex(cip95MetadataHash); - const anchor = Anchor.new(url, hash); + const anchor = generateAnchor(cip95MetadataURL, cip95MetadataHash); // Create cert object using one Ada as the deposit dRepUpdateCert = DrepUpdate.new_with_anchor(dRepCred, anchor); } else { @@ -1193,9 +1210,7 @@ function CardanoProvider(props: Props) { let votingProcedure; if (cip95MetadataURL && cip95MetadataHash) { - const url = URL.new(cip95MetadataURL); - const hash = AnchorDataHash.from_hex(cip95MetadataHash); - const anchor = Anchor.new(url, hash); + const anchor = generateAnchor(cip95MetadataURL, cip95MetadataHash); // Create cert object using one Ada as the deposit votingProcedure = VotingProcedure.new_with_anchor( votingChoice, @@ -1218,74 +1233,163 @@ function CardanoProvider(props: Props) { [dRepID] ); + const getRewardAddress = useCallback(async () => { + const addresses = await walletApi?.getRewardAddresses(); + if (!addresses) { + throw new Error("Can not get reward addresses from wallet."); + } + const firstAddress = addresses[0]; + const bech32Address = Address.from_bytes( + Buffer.from(firstAddress, "hex") + ).to_bech32(); + + return RewardAddress.from_address(Address.from_bech32(bech32Address)); + }, [walletApi]); + + // info action + const buildNewInfoGovernanceAction = useCallback( + async ({ hash, url }: InfoProps) => { + let govActionBuilder = VotingProposalBuilder.new(); + try { + // Create new info action + const infoAction = InfoAction.new(); + const infoGovAct = GovernanceAction.new_info_action(infoAction); + // Create an anchor + const anchor = generateAnchor(url, hash); + + const rewardAddr = await getRewardAddress(); + if (!rewardAddr) throw new Error("Can not get reward address"); + + // Create voting proposal + const votingProposal = VotingProposal.new( + infoGovAct, + anchor, + rewardAddr, + BigNum.from_str(epochParams.gov_action_deposit.toString()) + ); + govActionBuilder.add(votingProposal); + + return govActionBuilder; + } catch (err) { + console.error(err); + } + }, + [epochParams, getRewardAddress] + ); + + // treasury action + const buildTreasuryGovernanceAction = useCallback( + async ({ amount, hash, receivingAddress, url }: TreasuryProps) => { + const govActionBuilder = VotingProposalBuilder.new(); + try { + const treasuryTarget = RewardAddress.from_address( + Address.from_bech32(receivingAddress) + ); + + if (!treasuryTarget) throw new Error("Can not get tresasury target"); + + const myWithdrawal = BigNum.from_str(amount); + const withdrawals = TreasuryWithdrawals.new(); + withdrawals.insert(treasuryTarget, myWithdrawal); + // Create new treasury withdrawal gov act + const treasuryAction = TreasuryWithdrawalsAction.new(withdrawals); + const treasuryGovAct = + GovernanceAction.new_treasury_withdrawals_action(treasuryAction); + // Create an anchor + const anchor = generateAnchor(url, hash); + + const rewardAddr = await getRewardAddress(); + + if (!rewardAddr) throw new Error("Can not get reward address"); + // Create voting proposal + const votingProposal = VotingProposal.new( + treasuryGovAct, + anchor, + rewardAddr, + BigNum.from_str(epochParams.gov_action_deposit.toString()) + ); + govActionBuilder.add(votingProposal); + + return govActionBuilder; + } catch (err) { + console.error(err); + } + }, + [epochParams, getRewardAddress] + ); + const value = useMemo( () => ({ address, - enable, - voter, - isEnabled, - isMainnet, - disconnectWallet, - dRepID, - dRepIDBech32, - pubDRepKey, - stakeKey, - setVoter, - setStakeKey, - stakeKeys, - walletApi, - error, - delegatedDRepID, - setDelegatedDRepID, - buildSignSubmitConwayCertTx, buildDRepRegCert, - buildDRepUpdateCert, buildDRepRetirementCert, + buildDRepUpdateCert, + buildNewInfoGovernanceAction, + buildSignSubmitConwayCertTx, + buildTreasuryGovernanceAction, buildVote, buildVoteDelegationCert, - delegateTransaction, - registerTransaction, - soleVoterTransaction, + delegatedDRepID, delegateTo, - voteTransaction, - isPendingTransaction, - isDrepLoading, - setIsDrepLoading, - isEnableLoading, - }), - [ - address, - enable, - voter, - isEnabled, - isMainnet, + delegateTransaction, disconnectWallet, dRepID, dRepIDBech32, + enable, + error, + isDrepLoading, + isEnabled, + isEnableLoading, + isMainnet, + isPendingTransaction, pubDRepKey, - stakeKey, - setVoter, + registerTransaction, + setDelegatedDRepID, + setIsDrepLoading, setStakeKey, + setVoter, + soleVoterTransaction, + stakeKey, stakeKeys, + voter, + voteTransaction, walletApi, - error, - delegatedDRepID, - setDelegatedDRepID, - buildSignSubmitConwayCertTx, + }), + [ + address, buildDRepRegCert, - buildDRepUpdateCert, buildDRepRetirementCert, + buildDRepUpdateCert, + buildNewInfoGovernanceAction, + buildSignSubmitConwayCertTx, + buildTreasuryGovernanceAction, buildVote, buildVoteDelegationCert, + delegatedDRepID, + delegateTo, delegateTransaction, + disconnectWallet, + dRepID, + dRepIDBech32, + enable, + error, + isDrepLoading, + isEnabled, + isEnableLoading, + isMainnet, + isPendingTransaction, + pubDRepKey, registerTransaction, + setDelegatedDRepID, + setIsDrepLoading, + setStakeKey, + setVoter, soleVoterTransaction, - delegateTo, + stakeKey, + stakeKeys, + voter, voteTransaction, - isPendingTransaction, - isDrepLoading, - setIsDrepLoading, - isEnableLoading, + walletApi, ] ); diff --git a/govtool/frontend/src/hooks/forms/index.ts b/govtool/frontend/src/hooks/forms/index.ts index 2457ac6de..fe994e70b 100644 --- a/govtool/frontend/src/hooks/forms/index.ts +++ b/govtool/frontend/src/hooks/forms/index.ts @@ -1,3 +1,4 @@ +export * from "./useCreateGovernanceActionForm"; export * from "./useDelegateTodRepForm"; export * from "./useRegisterAsdRepFormContext"; export * from "./useUpdatedRepMetadataForm"; diff --git a/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts new file mode 100644 index 000000000..614e3d634 --- /dev/null +++ b/govtool/frontend/src/hooks/forms/useCreateGovernanceActionForm.ts @@ -0,0 +1,108 @@ +import { + GovernanceActionFieldSchemas, + GovernanceActionType, +} from "@/types/governanceAction"; +import { useCallback, useState } from "react"; +import { useFormContext } from "react-hook-form"; + +import { CIP_100, CIP_108, GOVERNANCE_ACTION_CONTEXTS } from "@consts"; + +import * as jsonld from "jsonld"; + +export type CreateGovernanceActionValues = { + links?: { link: string }[]; + storeData?: boolean; + storingURL: string; + governance_action_type?: GovernanceActionType; +} & Partial>; + +export const defaulCreateGovernanceActionValues: CreateGovernanceActionValues = + { + links: [{ link: "" }], + storeData: false, + storingURL: "", + }; + +export const useCreateGovernanceActionForm = () => { + const [isLoading, setIsLoading] = useState(false); + + const { + control, + formState: { errors, isValid }, + getValues, + handleSubmit, + setValue, + watch, + register, + reset, + } = useFormContext(); + + const govActionType = watch("governance_action_type"); + + // TODO: To be moved to utils + const generateJsonBody = async (data: CreateGovernanceActionValues) => { + const filteredData = Object.entries(data) + .filter( + ([key]) => + !Object.keys(defaulCreateGovernanceActionValues).includes(key) && + key !== "governance_action_type" + ) + .map(([key, value]) => { + return [CIP_108 + key, value]; + }); + + const references = (data as CreateGovernanceActionValues).links + ?.filter((link) => link.link) + .map((link) => { + return { + [`@type`]: "Other", + [`${CIP_100}reference-label`]: "Label", + [`${CIP_100}reference-uri`]: link.link, + }; + }); + + const body = { + ...Object.fromEntries(filteredData), + [`${CIP_108}references`]: references, + }; + + const doc = { + [`${CIP_108}body`]: body, + [`${CIP_100}hashAlgorithm`]: "blake2b-256", + [`${CIP_100}authors`]: [], + }; + + const json = await jsonld.compact( + doc, + GOVERNANCE_ACTION_CONTEXTS[govActionType as GovernanceActionType] + ); + + return json; + }; + + const onSubmit = useCallback(async (data: CreateGovernanceActionValues) => { + try { + setIsLoading(true); + const jsonBody = generateJsonBody(data); + + return jsonBody; + } catch (e: any) { + } finally { + setIsLoading(false); + } + }, []); + + return { + control, + errors, + getValues, + isLoading, + isValid, + setValue, + createGovernanceAction: handleSubmit(onSubmit), + watch, + register, + reset, + generateJsonBody, + }; +}; diff --git a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx index cf1a0cbe0..30fd331aa 100644 --- a/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx +++ b/govtool/frontend/src/hooks/forms/useVoteActionForm.tsx @@ -110,7 +110,7 @@ export const useVoteActionForm = () => { }); if (result) { addSuccessAlert("Vote submitted"); - navigate(PATHS.dashboard_governance_actions, { + navigate(PATHS.dashboardGovernanceActions, { state: { isVotedListOnLoad: state && state.vote ? true : false, }, diff --git a/govtool/frontend/src/i18n/locales/en.ts b/govtool/frontend/src/i18n/locales/en.ts index f205ffcca..edc1e2be7 100644 --- a/govtool/frontend/src/i18n/locales/en.ts +++ b/govtool/frontend/src/i18n/locales/en.ts @@ -45,6 +45,7 @@ export const en = { dashboard: { headingOne: "Your Participation", headingTwo: "See Active Governance Actions", + title: "Dashboard", delegation: { changeDelegation: "Change delegation", delegateOwnPower: @@ -79,6 +80,12 @@ export const en = { title: "Governance Actions", view: "View governance actions", }, + proposeGovernanceAction: { + title: "Propose a Governance Action", + description: "Submit your proposal for a Governance Action.", + propose: "Propose", + view: "View governance actions", + }, registration: { changeMetadata: "Change metadata", dRepRegistration: "DRep Registration", @@ -121,6 +128,70 @@ export const en = { youAreSoleVoterTitle: "You are a Sole Voter", }, }, + createGovernanceAction: { + chooseGATypeTitle: "Choose a Governance Action type", + creatingAGovernanceAction: + "Creating a Governance Action: What you need to know", + creatingAGovernanceActionDescription: + "To create a Governance Action, you will need to:\n\n• Fill out a form with the relevant data\n• Pay a refundable deposit of ₳{{deposit}}\n• Store the metadata of your Governance Action at your own expense.\n\nYour deposit will be refunded to your wallet when the Governance Action is either enacted or expired.\n\nThe deposit will not affect your Voting Power.", + editSubmission: "Edit submission", + formTitle: "Governance Action details", + references: "References and Supporting Information", + reviewSubmission: "Review your submission", + storeDataCheckboxLabel: + "I agree to store correctly this information and to maintain them over the years", + storeDataLink: "Learn more about storing information", + storeDataTitle: "Store and Maintain the Data Yourself", + storingInformationDescription: + "Download your file, save it to your chosen location, and enter the URL of that location in step 3", + storingInformationStep1Label: "Download this file", + storingInformationStep2Label: + "Save this file in a location that provides a public URL (ex. github)", + storingInformationStep2Link: "Read full guide", + storingInformationStep3Label: "Paste the URL here", + storingInformationTitle: "Information Storage Steps", + storingInformationURLPlaceholder: "URL", + supportingLinks: "Supporting links", + title: "Create a Governance Action", + fields: { + declarations: { + title: { + label: "Title", + placeholder: "A name for this Action", + }, + abstract: { + label: "Abstract", + placeholder: "Summary", + tip: "General summary of the Action", + }, + motivation: { + label: "Motivation", + placeholder: "Problem this GA will solve", + tip: "How will this solve a problem", + }, + rationale: { + label: "Rationale", + placeholder: "Content of Governance Action", + tip: "Put all the content of the GA here", + }, + receivingAddress: { + label: "Receiving Address", + placeholder: "The address to receive funds", + }, + amount: { + label: "Amount", + placeholder: "e.g. 20000", + }, + }, + validations: { + maxLength: "Max {{maxLength}} characters", + required: "This field is required", + bech32: "Invalid bech32 address", + number: "Only number is allowed", + url: "Invalid URL", + }, + }, + }, delegation: { description: "You can delegate your voting power to a DRep or to a pre-defined voting option.", @@ -182,8 +253,14 @@ export const en = { forms: { hashPlaceholder: "The hash of metadata at URL", howCreateUrlAndHash: "How to create URL and hash?", + link: "Link", urlWithContextPlaceholder: "Your URL with with your context", urlWithInfoPlaceholder: "Your URL with extra info about you", + createGovernanceAction: { + typeLabel: "Governance Action Type", + typeTip: + "To change the Governance Action Type go back to the previous page.", + }, errors: { hashInvalidFormat: "Invalid hash format", hashInvalidLength: "Hash must be exactly 64 characters long", @@ -235,37 +312,45 @@ export const en = { }, hero: { connectWallet: "Connect your wallet", - description: { - mobile: - "You can either delegate your voting power or become a DRep to allow people to delegate voting power to you.", - wide: "Anyone with a wallet containing ADA can participate in governance on Sanchonet.\n\nYour ADA balance entitles you to an equal amount of Voting Power.\n\nFor more info see the guide entry for <0>Voting Power.", - }, + description: + "Anyone with a wallet containing ADA can participate in governance on Sanchonet.\n\nYour ADA balance entitles you to an equal amount of Voting Power.\n\nFor more info see the guide entry for <0>Voting Power.", headline: "SanchoNet \n Governance Tool", }, home: { cards: { - delegateDescription: "Find a DRep to vote on your behalf.", - delegateFirstButtonLabel: "View DRep Direcotry", - delegateTitle: "Delegate your Voting Power", - governaneActionsDescription: - "See all the Governance Actions submitted on chain. ", - governanceActionsFirstButtonLabel: "View Governance Actions", - governaneActionsTitle: "View Governance Actions", - registerAsDRepDescription: - "Accept delegated voting power from other ADA holders, and combine it with your own voting power. Vote with the accumulated Power on Governance Actions.", - registerAsDRepFirstButtonLabel: "Connect to Register", - registerAsDRepTitle: "Become a DRep", - registerAsSoleVoterDescription: - "Vote on Governance Actions using your own voting power", - registerAsSoleVoterFirstButtonLabel: "Connect to Register", - registerAsSoleVoterTitle: "Become a Sole Voter", + delegate: { + description: "Find a DRep to vote on your behalf.", + firstButtonLabel: "View DRep Directory", + title: "Delegate your Voting Power", + }, + governanceActions: { + description: "See all the Governance Actions submitted on chain. ", + firstButtonLabel: "View Governance Actions", + title: "View Governance Actions", + }, + proposeAGovernanceAction: { + description: "Submit your proposal for a Governance Action.", + firstButtonLabel: "Connect to Submit", + title: "Propose a Governance Action", + }, + registerAsDRep: { + description: + "Accept delegated voting power from other ADA holders, and combine it with your own voting power. Vote with the accumulated Power on Governance Actions.", + firstButtonLabel: "Connect to Register", + title: "Become a DRep", + }, + registerAsSoleVoter: { + description: "Vote on Governance Actions using your own voting power", + firstButtonLabel: "Connect to Register", + title: "Become a Sole Voter", + }, }, }, menu: { faqs: "FAQs", guides: "Guides", help: "Help", - myDashboard: "My Dashboard", + dashboard: "Dashboard", viewGovActions: "View Governance Actions", }, metadataUpdate: { @@ -279,6 +364,12 @@ export const en = { goToDashboard: "Go to Dashboard", oops: "Oops!", }, + createGovernanceAction: { + cancelModalDescription: + "Returning to the Dashboard will cancel your submission and your proposed Governance Action will not be submitted.", + cancelModalTitle: + "Do you want to Cancel your Governance Action submission?", + }, delegation: { message: "The confirmation of your actual delegation might take a bit of time but you can track it using", @@ -294,6 +385,9 @@ export const en = { youAreAboutToOpen: "You are about to open an external link to:", }, registration: { + cancelTitle: "Do You Want to Abandon Registration ?", + cancelDescription: + "If you return to the Dashboard, your information will not be saved.", message: "The confirmation of your registration might take a bit of time but you can track it using", title: "Registration Transaction Submitted!", @@ -417,6 +511,7 @@ export const en = { "Warning, no registered stake keys, using unregistered stake keys", }, abstain: "Abstain", + addLink: "+ Add link", back: "Back", backToDashboard: "Back to dashboard", backToList: "Back to the list", @@ -433,7 +528,9 @@ export const en = { nextStep: "Next step", no: "No", ok: "Ok", - register:"Register", + optional: "Optional", + register: "Register", + required: "required", seeTransaction: "See transaction", select: "Select", skip: "Skip", diff --git a/govtool/frontend/src/models/wallet.ts b/govtool/frontend/src/models/wallet.ts index ebba3d0c1..2baf480c2 100644 --- a/govtool/frontend/src/models/wallet.ts +++ b/govtool/frontend/src/models/wallet.ts @@ -94,7 +94,7 @@ export interface CardanoApiWallet { getUsedAddresses(): Promise; getUnusedAddresses(): Promise; getChangeAddress(): Promise; - getRewardAddress(): Promise; + getRewardAddresses(): Promise; getNetworkId(): Promise; signData(arg0: any, arg1?: any): Promise; signTx(arg0: any, arg1?: any): Promise; diff --git a/govtool/frontend/src/pages/CreateGovernanceAction.tsx b/govtool/frontend/src/pages/CreateGovernanceAction.tsx new file mode 100644 index 000000000..5ebf6a602 --- /dev/null +++ b/govtool/frontend/src/pages/CreateGovernanceAction.tsx @@ -0,0 +1,95 @@ +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { FormProvider, useForm } from "react-hook-form"; +import { Box } from "@mui/material"; + +import { Background } from "@atoms"; +import { PATHS } from "@consts"; +import { useModal } from "@context"; +import { + defaulCreateGovernanceActionValues, + useScreenDimension, + useTranslation, +} from "@hooks"; +import { LinkWithIcon } from "@molecules"; +import { + ChooseGovernanceActionType, + CreateGovernanceActionForm, + DashboardTopNav, + Footer, + ReviewCreatedGovernanceAction, + StorageInformation, + StoreDataInfo, + WhatGovernanceActionIsAbout, +} from "@organisms"; +import { checkIsWalletConnected } from "@utils"; + +export const CreateGovernanceAction = () => { + const [step, setStep] = useState(1); + const navigate = useNavigate(); + const { t } = useTranslation(); + const { isMobile } = useScreenDimension(); + const { closeModal, openModal } = useModal(); + + const methods = useForm({ + mode: "onChange", + defaultValues: defaulCreateGovernanceActionValues, + }); + + useEffect(() => { + if (checkIsWalletConnected()) { + navigate(PATHS.home); + } + }, []); + + const onClickBackToDashboard = () => + openModal({ + type: "statusModal", + state: { + status: "warning", + message: t("modals.createGovernanceAction.cancelModalDescription"), + buttonText: t("modals.common.goToDashboard"), + title: t("modals.createGovernanceAction.cancelModalTitle"), + dataTestId: "cancel-governance-action-creation-modal", + onSubmit: backToDashboard, + }, + }); + + const backToDashboard = () => { + navigate(PATHS.dashboard); + closeModal(); + }; + + return ( + + + + + + {step === 1 && ( + + )} + {step === 2 && } + {step === 3 && } + {step === 4 && } + {step === 5 && } + {step === 6 && } + + {isMobile &&