From 73fce288221fb827cedf2330dc4dbd6f2fc74c6b Mon Sep 17 00:00:00 2001 From: Vitor Marthendal Nunes Date: Mon, 11 Sep 2023 22:02:17 -0300 Subject: [PATCH] Feat: Add latest turboETH updates (#18) * feat: add template updates * fix: remove .github from gitignore --- .gitignore | 1 - src/commands/index.ts | 2 +- src/config/integrations.ts | 124 +- src/types.ts | 3 + template/base/.env.example | 4 + template/base/.eslintignore | 1 + template/base/.eslintrc.cjs | 38 + template/base/.eslintrc.json | 77 - template/base/{ => .github}/workflows/ci.yml | 6 +- template/base/.prettierignore | 12 +- template/base/.prettierrc.json | 9 - template/base/.vscode/settings.json | 15 +- template/base/README.md | 13 +- template/base/app/(general)/account/page.tsx | 43 - .../sign-in-with-ethereum/opengraph-image.tsx | 6 +- .../sign-in-with-ethereum/page.tsx | 106 +- .../sign-in-with-ethereum/twitter-image.tsx | 4 +- template/base/app/(general)/layout.tsx | 35 +- template/base/app/(general)/page.tsx | 491 +-- template/base/app/admin/layout.tsx | 67 - template/base/app/admin/page.tsx | 35 - template/base/app/api/app/user/route.ts | 8 +- template/base/app/api/app/users/route.ts | 12 +- template/base/app/api/siwe/logout/route.ts | 2 +- template/base/app/api/siwe/nonce/route.ts | 2 +- template/base/app/api/siwe/route.ts | 2 +- template/base/app/api/siwe/verify/route.ts | 2 +- template/base/app/dashboard/account/page.tsx | 34 +- template/base/app/dashboard/admin/page.tsx | 44 + template/base/app/dashboard/layout.tsx | 114 +- template/base/app/dashboard/page.tsx | 35 +- .../base/app/dashboard/transactions/page.tsx | 38 +- template/base/app/layout.tsx | 56 +- template/base/app/opengraph-image.png | Bin 0 -> 232895 bytes template/base/app/opengraph-image.tsx | 84 +- template/base/app/twitter-image.tsx | 13 - template/base/commitlint.config.js | 2 +- .../base/components/app/app-users-table.tsx | 46 +- .../base/components/blockchain/address.tsx | 28 +- .../blockchain/block-explorer-link.tsx | 29 +- .../blockchain/contract-write-button.tsx | 27 +- .../blockchain/handle-wallet-events.tsx | 10 +- .../components/blockchain/network-status.tsx | 60 +- .../blockchain/transaction-status.tsx | 32 +- .../components/blockchain/wallet-address.tsx | 27 +- .../components/blockchain/wallet-balance.tsx | 16 +- .../blockchain/wallet-connect-custom.tsx | 52 +- .../components/blockchain/wallet-connect.tsx | 18 +- .../components/blockchain/wallet-ens-name.tsx | 10 +- .../components/blockchain/wallet-nonce.tsx | 9 +- .../components/layout/dashboard-footer.tsx | 30 - .../components/layout/dashboard-header.tsx | 73 - template/base/components/layout/footer.tsx | 33 +- template/base/components/layout/header.tsx | 72 - template/base/components/layout/main-nav.tsx | 141 + .../components/layout/menu-admin-sidebar.tsx | 43 - .../layout/menu-dashboard-sidebar.tsx | 44 - .../base/components/layout/mobile-nav.tsx | 224 + .../layout/navigation-menu-general.tsx | 121 - .../base/components/layout/page-header.tsx | 85 + .../base/components/layout/page-section.tsx | 49 + .../base/components/layout/sidebar-nav.tsx | 47 + .../base/components/layout/site-header.tsx | 37 + .../base/components/layout/user-dropdown.tsx | 54 - .../base/components/providers/rainbow-kit.tsx | 38 +- .../components/providers/root-provider.tsx | 24 +- template/base/components/shared/card.tsx | 62 - .../base/components/shared/copy-button.tsx | 55 + .../base/components/shared/example-demos.tsx | 600 +++ template/base/components/shared/icons.tsx | 23 - .../base/components/shared/is-dark-theme.tsx | 11 +- .../base/components/shared/is-desktop.tsx | 17 - .../base/components/shared/is-light-theme.tsx | 11 +- template/base/components/shared/is-mobile.tsx | 17 - .../components/shared/is-wallet-connected.tsx | 7 +- .../shared/is-wallet-disconnected.tsx | 11 +- .../components/shared/light-dark-image.tsx | 42 + .../base/components/shared/link-component.tsx | 31 +- .../base/components/shared/mode-toggle.tsx | 41 + .../components/shared/table/table-body.tsx | 28 +- .../components/shared/table/table-core.tsx | 28 +- .../components/shared/table/table-head.tsx | 27 +- .../shared/table/table-pagination.tsx | 90 +- .../base/components/shared/theme-toggle.tsx | 65 - .../components/shared/time-from-epoch.tsx | 17 +- .../base/components/shared/time-from-utc.tsx | 11 +- template/base/components/ui/accordion.tsx | 84 +- template/base/components/ui/alert-dialog.tsx | 181 +- template/base/components/ui/aspect-ratio.tsx | 5 - template/base/components/ui/avatar.tsx | 66 +- template/base/components/ui/badge.tsx | 36 + template/base/components/ui/button.tsx | 62 +- template/base/components/ui/card.tsx | 76 + template/base/components/ui/checkbox.tsx | 43 +- template/base/components/ui/collapsible.tsx | 9 - template/base/components/ui/context-menu.tsx | 175 - template/base/components/ui/dialog.tsx | 156 +- template/base/components/ui/dropdown-menu.tsx | 123 +- template/base/components/ui/form.tsx | 131 +- template/base/components/ui/hover-card.tsx | 27 - template/base/components/ui/input.tsx | 36 +- template/base/components/ui/label.tsx | 30 +- template/base/components/ui/menubar.tsx | 202 - .../base/components/ui/navigation-menu.tsx | 113 +- template/base/components/ui/popover.tsx | 40 +- template/base/components/ui/progress.tsx | 38 +- template/base/components/ui/radio-group.tsx | 34 - template/base/components/ui/scroll-area.tsx | 53 +- template/base/components/ui/select.tsx | 160 +- template/base/components/ui/separator.tsx | 23 +- template/base/components/ui/sheet.tsx | 144 + template/base/components/ui/skeleton.tsx | 15 + template/base/components/ui/slider.tsx | 19 - .../ui/social/og-image-integrations.tsx | 87 +- template/base/components/ui/switch.tsx | 39 +- template/base/components/ui/tabs.tsx | 80 +- template/base/components/ui/textarea.tsx | 35 +- template/base/components/ui/toast.tsx | 155 +- template/base/components/ui/toaster.tsx | 17 +- template/base/components/ui/tooltip.tsx | 45 +- template/base/components/ui/use-toast.tsx | 189 + template/base/config/design.ts | 4 +- template/base/config/menu-admin.ts | 4 +- template/base/config/menu-dashboard.ts | 12 +- template/base/config/networks.ts | 79 +- template/base/config/site.ts | 25 +- template/base/data/turbo-integrations.ts | 277 +- template/base/env.mjs | 20 +- template/base/integrations/siwe/README.md | 3 - .../integrations/siwe/actions/siwe-login.ts | 28 +- .../integrations/siwe/actions/siwe-logout.ts | 4 +- .../integrations/siwe/actions/siwe-message.ts | 18 +- template/base/integrations/siwe/api/index.ts | 4 +- template/base/integrations/siwe/api/logout.ts | 4 +- template/base/integrations/siwe/api/nonce.ts | 8 +- template/base/integrations/siwe/api/verify.ts | 20 +- .../branch-button-login-or-account.tsx | 33 - .../siwe/components/button-siwe-login.tsx | 44 +- .../siwe/components/button-siwe-logout.tsx | 26 +- .../siwe/components/is-signed-in.tsx | 6 +- .../siwe/components/is-signed-out.tsx | 6 +- template/base/lib/app/get-app-users.ts | 10 +- template/base/lib/fonts.ts | 11 + template/base/lib/generated/blockchain.ts | 443 +- .../base/lib/hooks/app/use-get-app-users.ts | 6 +- .../lib/hooks/use-intersection-observer.ts | 9 +- template/base/lib/hooks/use-is-mounted.tsx | 2 +- template/base/lib/hooks/use-scroll.ts | 6 +- template/base/lib/hooks/use-toast.ts | 49 +- template/base/lib/hooks/use-user.tsx | 15 +- .../lib/hooks/web3/use-ethers-provider.ts | 15 +- .../base/lib/hooks/web3/use-ethers-signer.ts | 12 +- template/base/lib/prisma.ts | 2 +- template/base/lib/session.ts | 15 +- template/base/lib/state/color-mode.ts | 12 +- template/base/lib/utils/get-network-color.ts | 12 +- template/base/lib/utils/index.ts | 30 +- template/base/lib/utils/motion.ts | 47 + template/base/lint-staged.config.js | 6 +- template/base/next.config.mjs | 29 +- template/base/package.json | 24 +- template/base/pnpm-lock.yaml | 2674 ++++++++---- template/base/prettier.config.js | 40 +- template/base/public/favicon.ico | Bin 57751 -> 26374 bytes template/base/public/icon-192x192.png | Bin 0 -> 26374 bytes template/base/public/icon-256x256.png | Bin 0 -> 44560 bytes template/base/public/icon-384x384.png | Bin 0 -> 67343 bytes template/base/public/icon-512x512.png | Bin 0 -> 104509 bytes template/base/public/integrations/disco.jpeg | Bin 16650 -> 0 bytes .../base/public/integrations/discoDark.png | Bin 0 -> 97586 bytes .../{disco.png => discoLight.png} | Bin .../base/public/integrations/erc1155-icon.png | Bin 0 -> 157006 bytes .../base/public/integrations/erc20-icon.png | Bin 9881 -> 157006 bytes template/base/public/integrations/erc20.png | Bin 0 -> 68865 bytes .../public/integrations/gitcoin-passport.svg | 9 + .../public/integrations/lensprotocol-dark.svg | 3 + .../integrations/lensprotocol-light.svg | 3 + template/base/public/logo-dark.png | Bin 5504 -> 86427 bytes template/base/public/logo-fill.png | Bin 54854 -> 0 bytes template/base/public/logo-gradient.png | Bin 50735 -> 104509 bytes template/base/public/logo-light.png | Bin 0 -> 83716 bytes template/base/public/logo-white.png | Bin 5556 -> 0 bytes template/base/public/logo.png | Bin 97537 -> 0 bytes template/base/public/manifest.json | 33 + template/base/public/preview.png | Bin 321299 -> 0 bytes template/base/styles/app.css | 110 - template/base/styles/globals.css | 89 + template/base/styles/gradient.css | 27 - template/base/styles/periphery.css | 90 - template/base/tailwind.config.js | 136 +- template/base/tsconfig.json | 22 +- template/base/wagmi.config.ts | 10 +- .../aave/core/aave/abis/pool-abi.ts | 1706 ++++---- .../aave/abis/ui-pool-data-provider-abi.ts | 428 +- .../aave/components/asset-to-borrow-item.tsx | 140 +- .../aave/components/asset-to-supply-item.tsx | 167 +- .../aave/components/borrowed-assets-item.tsx | 257 +- .../core/aave/components/general-info.tsx | 88 +- .../core/aave/components/health-factor.tsx | 8 +- .../aave/components/list-assets-to-borrow.tsx | 42 +- .../aave/components/list-assets-to-supply.tsx | 44 +- .../aave/components/list-borrowed-assets.tsx | 89 +- .../aave/components/list-supplied-assets.tsx | 59 +- .../aave/core/aave/components/spinner.tsx | 12 +- .../aave/components/supplied-assets-item.tsx | 140 +- .../aave/core/aave/generated/aave-wagmi.ts | 3653 +++++++++++------ .../aave/core/aave/hooks/use-aave.ts | 92 +- .../aave/core/aave/utils/index.ts | 8 +- .../aave/core/aave/utils/market-config.ts | 59 +- .../aave/core/aave/wagmi.config.ts | 14 +- .../integrations/aave/pages/aave/layout.tsx | 63 - .../aave/pages/aave/opengraph-image.tsx | 6 +- .../integrations/aave/pages/aave/page.tsx | 119 +- .../aave/pages/aave/twitter-image.tsx | 4 +- .../arweave/core/arweave/arweave-account.ts | 46 +- .../arweave-account/form/controls.ts | 104 +- .../components/arweave-account/form/hook.ts | 54 +- .../components/arweave-account/form/index.tsx | 161 +- .../components/arweave-account/index.tsx | 264 +- .../arweave-account/sidebar-preview.tsx | 33 +- .../components/connect-arweave-wallet.tsx | 48 +- .../arweave/components/fee-estimation.tsx | 9 +- .../arweave/components/form-new-post/hook.ts | 74 +- .../components/form-new-post/index.tsx | 272 +- .../components/form-new-post/list-tags.tsx | 49 +- .../components/insufficient-balance-error.tsx | 15 +- .../core/arweave/components/list-posts.tsx | 170 +- .../core/arweave/components/pending-tx.tsx | 87 +- .../arweave/core/arweave/components/post.tsx | 287 +- .../core/arweave/components/settings.tsx | 14 +- .../core/arweave/components/spinner.tsx | 12 +- .../core/arweave/context/arweave-wallet.tsx | 111 +- .../core/arweave/hooks/use-arweave-wallet.ts | 7 +- .../core/arweave/hooks/use-estimate-tx-fee.ts | 19 +- .../core/arweave/hooks/use-get-posts.ts | 60 +- .../arweave/core/arweave/index.ts | 48 +- .../core/arweave/queries/query-post.ts | 9 +- .../core/arweave/queries/query-posts.ts | 29 +- .../arweave/utils/get-element-component.ts | 4 +- .../arweave/core/arweave/utils/index.ts | 27 +- .../arweave/core/arweave/utils/types.ts | 5 +- .../pages/arweave/account/edit/page.tsx | 5 +- .../arweave/pages/arweave/account/page.tsx | 5 +- .../arweave/pages/arweave/layout.tsx | 114 +- .../arweave/pages/arweave/opengraph-image.tsx | 6 +- .../arweave/pages/arweave/page.tsx | 5 +- .../pages/arweave/posts/[txId]/page.tsx | 6 +- .../arweave/pages/arweave/posts/new/page.tsx | 5 +- .../arweave/pages/arweave/posts/page.tsx | 5 +- .../arweave/pages/arweave/settings/page.tsx | 4 +- .../arweave/pages/arweave/sidebar.tsx | 93 +- .../arweave/pages/arweave/twitter-image.tsx | 4 +- .../api/connext/approve-if-needed/route.ts | 2 +- .../api/connext/estimated-amount/route.ts | 2 +- .../connext/estimated-relayer-fee/route.ts | 2 +- .../api/connext/latest-transfers/route.ts | 2 +- .../connext/api/connext/xcall/route.ts | 2 +- .../connext/core/connext/README.md | 34 +- .../core/connext/api/approve-if-needed.tsx | 40 +- .../core/connext/api/estimated-amount.tsx | 67 +- .../connext/api/estimated-relayer-fee.tsx | 36 +- .../core/connext/api/latest-transfers.tsx | 18 +- .../connext/core/connext/api/xcall.tsx | 77 +- .../connext/core/connext/client.ts | 26 +- .../components/form-connext-xtransfer.tsx | 392 +- .../connext/components/latest-transfers.tsx | 55 +- .../core/connext/components/spinner.tsx | 12 +- .../core/connext/components/transfer.tsx | 155 +- .../connext/hooks/use-approve-if-needed.ts | 43 +- .../connext/hooks/use-estimated-amount.ts | 63 +- .../hooks/use-estimated-relayer-fee.ts | 40 +- .../connext/hooks/use-latest-transfers.ts | 34 +- .../connext/hooks/use-supported-transfer.ts | 18 +- .../connext/core/connext/hooks/use-xcall.ts | 74 +- .../core/connext/utils/assets/index.ts | 4 +- .../core/connext/utils/assets/mainnet.ts | 272 +- .../core/connext/utils/assets/testnet.ts | 112 +- .../core/connext/utils/chains/index.ts | 4 +- .../core/connext/utils/chains/mainnet.ts | 264 +- .../core/connext/utils/chains/testnet.ts | 204 +- .../connext/core/connext/utils/constants.ts | 8 +- .../connext/core/connext/utils/index.ts | 44 +- .../connext/pages/connext/layout.tsx | 53 - .../connext/pages/connext/opengraph-image.tsx | 6 +- .../connext/pages/connext/page.tsx | 76 +- .../connext/pages/connext/twitter-image.tsx | 4 +- .../api/disco/credentials-from-did/route.ts | 2 +- .../api/disco/issue-proof-of-hack/route.ts | 2 +- .../api/disco/profile-from-address/route.ts | 2 +- .../disco/api/disco/profile-from-did/route.ts | 2 +- .../core/disco/api/credentials-from-did.ts | 17 +- .../core/disco/api/issue-proof-of-hack.ts | 28 +- .../core/disco/api/profile-from-address.ts | 13 +- .../disco/core/disco/api/profile-from-did.ts | 17 +- .../disco/components/disco-profile-basic.tsx | 74 +- .../components/disco-profile-credentials.tsx | 91 +- .../form-issue-proof-of-hack/controls.ts | 76 +- .../form-issue-proof-of-hack/hook.ts | 39 +- .../form-issue-proof-of-hack/index.tsx | 158 +- .../disco/core/disco/disco-client.ts | 9 +- .../use-disco-get-credentials-from-did.ts | 8 +- .../use-disco-get-profile-from-address.ts | 15 +- .../hooks/use-disco-get-profile-from-did.ts | 8 +- .../hooks/use-disco-issue-proof-of-hack.ts | 6 +- .../routes/get-credentials-from-did/client.ts | 18 +- .../routes/get-credentials-from-did/index.ts | 8 +- .../routes/get-profile-from-address/client.ts | 21 +- .../routes/get-profile-from-address/index.ts | 10 +- .../routes/get-profile-from-did/client.ts | 21 +- .../routes/get-profile-from-did/index.ts | 4 +- .../routes/issue-proof-of-hack/client.ts | 10 +- .../disco/routes/issue-proof-of-hack/index.ts | 20 +- .../disco/core/disco/utils/constants.ts | 5 +- .../core/disco/utils/get-element-component.ts | 6 +- .../disco/core/disco/utils/types.ts | 6 +- .../integrations/disco/pages/disco/layout.tsx | 72 - .../disco/pages/disco/opengraph-image.tsx | 6 +- .../integrations/disco/pages/disco/page.tsx | 131 +- .../disco/pages/disco/profile/page.tsx | 47 - .../disco/pages/disco/proof-of-hack/page.tsx | 32 - .../disco/pages/disco/twitter-image.tsx | 4 +- .../erc1155/core/erc1155/README.md | 86 + .../erc1155/artifacts/core/erc1155-abi.ts | 753 ++++ .../artifacts/core/erc1155-bytecode.ts | 14 + .../erc1155/artifacts/test/erc1155-abi.ts | 742 ++++ .../artifacts/test/erc1155-bytecode.ts | 15 + .../components/erc1155-contract-uri.tsx | 19 + .../components/erc1155-deploy-test.tsx | 80 + .../erc1155/components/erc1155-deploy.tsx | 96 + .../core/erc1155/components/erc1155-name.tsx | 19 + .../erc1155/components/erc1155-owner-of.tsx | 27 + .../core/erc1155/components/erc1155-read.tsx | 103 + .../components/erc1155-set-token-storage.tsx | 52 + .../erc1155/components/erc1155-symbol.tsx | 20 + .../components/erc1155-token-total-supply.tsx | 27 + .../erc1155-token-uri-description.tsx | 22 + .../components/erc1155-token-uri-image.tsx | 46 + .../components/erc1155-token-uri-name.tsx | 24 + .../components/erc1155-token-uri-symbol.tsx | 24 + .../erc1155/components/erc1155-token-uri.tsx | 26 + .../erc1155-write-approve-for-all.tsx | 93 + .../components/erc1155-write-approve.tsx | 95 + .../erc1155-write-batch-transfer.tsx | 172 + .../erc1155/components/erc1155-write-mint.tsx | 116 + .../components/erc1155-write-transfer.tsx | 123 + .../core/erc1155/generated/erc1155-wagmi.ts | 813 ++++ .../erc1155/hooks/use-erc1155-metadata.ts | 69 + .../hooks/use-erc1155-token-storage.ts | 40 + .../erc1155/core/erc1155/index.ts | 18 + .../erc1155/core/erc1155/utils/types.ts | 7 + .../erc1155/core/erc1155/wagmi.config.ts | 15 + .../erc1155/pages/erc1155/opengraph-image.tsx | 9 + .../erc1155/pages/erc1155/page.tsx | 86 + .../erc1155/pages/erc1155/twitter-image.tsx | 9 + .../erc20/core/erc20/abis/erc20-abi.ts | 214 +- .../erc20/core/erc20/abis/erc20-bytecode.ts | 2 +- .../core/erc20/abis/erc20-mintable-abi.ts | 294 +- .../erc20/abis/erc20-mintable-bytecode.ts | 2 +- .../core/erc20/components/erc20-deploy.tsx | 76 +- .../erc20/components/erc20-event-mint.tsx | 19 +- .../erc20/components/erc20-event-transfer.tsx | 19 +- .../core/erc20/components/erc20-read.tsx | 121 +- .../components/erc20-set-token-storage.tsx | 52 +- .../erc20/components/erc20-write-mint.tsx | 82 +- .../erc20/components/erc20-write-transfer.tsx | 86 +- .../erc20/hooks/use-erc20-token-storage.ts | 17 +- .../erc20/core/erc20/wagmi.config.ts | 14 +- .../pages/erc20/[chainId]/[address]/page.tsx | 23 +- .../erc20/pages/erc20/opengraph-image.tsx | 6 +- .../integrations/erc20/pages/erc20/page.tsx | 163 +- .../erc20/pages/erc20/twitter-image.tsx | 4 +- .../erc721/core/erc721/abis/erc721-abi.ts | 484 +-- .../core/erc721/abis/erc721-bytecode.ts | 2 +- .../core/erc721/components/erc721-deploy.tsx | 89 +- .../core/erc721/components/erc721-name.tsx | 11 +- .../erc721/components/erc721-owner-of.tsx | 12 +- .../core/erc721/components/erc721-read.tsx | 105 +- .../components/erc721-set-token-storage.tsx | 52 +- .../core/erc721/components/erc721-symbol.tsx | 11 +- .../erc721-token-uri-description.tsx | 12 +- .../components/erc721-token-uri-image.tsx | 38 +- .../components/erc721-token-uri-name.tsx | 12 +- .../erc721/components/erc721-total-supply.tsx | 11 +- .../components/erc721-write-approve.tsx | 86 +- .../erc721/components/erc721-write-mint.tsx | 86 +- .../components/erc721-write-transfer.tsx | 123 +- .../core/erc721/hooks/use-erc721-metadata.ts | 47 +- .../erc721/hooks/use-erc721-token-storage.ts | 17 +- .../integrations/erc721/core/erc721/index.ts | 24 +- .../erc721/core/erc721/utils/types.ts | 5 +- .../erc721/core/erc721/wagmi.config.ts | 10 +- .../erc721/pages/erc721/opengraph-image.tsx | 6 +- .../integrations/erc721/pages/erc721/page.tsx | 139 +- .../erc721/pages/erc721/twitter-image.tsx | 4 +- .../etherscan/account/transactions/route.ts | 2 +- .../etherscan-account-transactions/client.ts | 15 +- .../etherscan-account-transactions/index.ts | 55 +- .../etherscan/api/account/transactions.ts | 35 +- .../components/transactions-table.tsx | 55 +- .../etherscan/core/etherscan/etherscan.d.ts | 2 +- .../use-etherscan-account-transactions.ts | 19 +- .../core/etherscan/utils/constants.ts | 66 +- .../etherscan/utils/get-chain-id-api-key.ts | 4 +- .../etherscan/utils/get-chain-id-api-url.ts | 4 +- .../etherscan/utils/get-etherscan-client.ts | 10 +- .../etherscan/utils/handle-error-types.ts | 2 +- .../utils/handle-etherscan-response.ts | 4 +- .../etherscan/utils/is-client-connected.ts | 10 +- .../core/etherscan/utils/is-valid-address.ts | 2 +- .../etherscan/utils/is-valid-api-service.ts | 5 +- .../utils/is-valid-chain-id-mapping.ts | 5 +- .../utils/is-valid-transaction-hash.ts | 2 +- .../etherscan/utils/query-etherscan-client.ts | 16 +- .../etherscan/core/etherscan/utils/types.ts | 2 +- .../pages/etherscan/opengraph-image.tsx | 6 +- .../etherscan/pages/etherscan/page.tsx | 166 +- .../pages/etherscan/twitter-image.tsx | 4 +- .../gelato/components/active-task-preview.tsx | 49 +- .../core/gelato/components/active-tasks.tsx | 54 +- .../components/create-task/contract-input.tsx | 71 +- .../components/create-task/create-task.tsx | 174 +- .../create-task/execution-values.tsx | 67 +- .../components/create-task/function-input.tsx | 55 +- .../create-task/hooks/use-wizard.ts | 93 +- .../gelato/components/create-task/index.ts | 2 +- .../components/create-task/interval-input.tsx | 140 +- .../components/create-task/payment-input.tsx | 48 +- .../components/create-task/resolver-input.tsx | 38 +- .../create-task/restriction-info.tsx | 38 +- .../create-task/task-name-input.tsx | 12 +- .../gelato/core/gelato/components/index.tsx | 8 +- .../core/gelato/components/rename-task.tsx | 77 +- .../task-view/executing-address.tsx | 21 +- .../components/task-view/function-data.tsx | 16 +- .../core/gelato/components/task-view/index.ts | 2 +- .../components/task-view/input-values.tsx | 6 +- .../components/task-view/interval-values.tsx | 32 +- .../components/task-view/payment-info.tsx | 10 +- .../components/task-view/resolver-values.tsx | 15 +- .../gelato/components/task-view/task-view.tsx | 168 +- .../gelato/core/gelato/graphql/codegen.ts | 13 +- .../gelato/core/gelato/graphql/tasks.graphql | 8 +- .../gelato/core/gelato/hooks/index.ts | 16 +- .../gelato/core/gelato/hooks/use-abi.ts | 28 +- .../core/gelato/hooks/use-active-tasks.ts | 35 +- .../core/gelato/hooks/use-automate-sdk.ts | 9 +- .../core/gelato/hooks/use-cancel-task.ts | 6 +- .../gelato/hooks/use-is-automate-supported.ts | 4 +- .../core/gelato/hooks/use-msg-sender.ts | 6 +- .../gelato/core/gelato/hooks/use-new-task.ts | 8 +- .../core/gelato/hooks/use-rename-task.ts | 6 +- .../core/gelato/hooks/use-task-resolver.ts | 14 +- .../gelato/core/gelato/hooks/use-task.ts | 31 +- .../integrations/gelato/core/gelato/index.ts | 4 +- .../gelato/core/gelato/utils/constants.ts | 145 +- .../gelato/core/gelato/utils/helpers.ts | 109 +- .../core/gelato/utils/resolverDecoder.ts | 12 +- .../gelato/core/gelato/utils/types.ts | 21 +- .../gelato/pages/gelato/layout.tsx | 74 - .../gelato/pages/gelato/opengraph-image.tsx | 6 +- .../integrations/gelato/pages/gelato/page.tsx | 85 +- .../gelato/pages/gelato/tasks/[id]/page.tsx | 10 +- .../gelato/pages/gelato/tasks/create/page.tsx | 15 +- .../gelato/pages/gelato/tasks/page.tsx | 4 +- .../gelato/pages/gelato/twitter-image.tsx | 4 +- .../gitcoin-passport/[address]/score/route.ts | 1 + .../[address]/stamps/route.ts | 1 + .../gitcoin-passport/signing-message/route.ts | 1 + .../gitcoin-passport/stamps-metadata/route.ts | 1 + .../gitcoin-passport/submit-passport/route.ts | 1 + .../core/gitcoin-passport/README.md | 62 + .../gitcoin-passport/api/address-score.ts | 36 + .../gitcoin-passport/api/address-stamps.ts | 26 + .../gitcoin-passport/api/signing-message.ts | 20 + .../gitcoin-passport/api/stamps-metadata.ts | 20 + .../gitcoin-passport/api/submit-passport.ts | 48 + .../components/list-stamps.tsx | 148 + .../components/score-gate.tsx | 49 + .../components/stamp-card.tsx | 210 + .../components/stamp-gate.tsx | 50 + .../components/submit-passport-button.tsx | 34 + .../hooks/use-get-address-stamps.ts | 48 + .../gitcoin-passport/hooks/use-get-score.ts | 39 + .../hooks/use-get-stamps-metadata.ts | 29 + .../hooks/use-submit-passport.ts | 89 + .../core/gitcoin-passport/utils/constants.ts | 3 + .../core/gitcoin-passport/utils/types.ts | 57 + .../pages/gitcoin-passport/dev-guide/page.tsx | 108 + .../pages/gitcoin-passport/layout.tsx | 60 + .../gitcoin-passport/opengraph-image.tsx | 9 + .../pages/gitcoin-passport/page.tsx | 22 + .../gitcoin-passport/score-gated/page.tsx | 46 + .../pages/gitcoin-passport/sidebar.tsx | 50 + .../gitcoin-passport/stamp-gated/page.tsx | 59 + .../pages/gitcoin-passport/twitter-image.tsx | 9 + .../core/lens-protocol/README.md | 74 + .../components/auth/is-user-authenticated.tsx | 17 + .../components/auth/login-button.tsx | 44 + .../components/auth/logout-button.tsx | 18 + .../components/auth/not-authenticated-yet.tsx | 8 + .../core/lens-protocol/components/feed.tsx | 41 + .../components/load-more-button.tsx | 18 + .../core/lens-protocol/components/navbar.tsx | 116 + .../components/profile/address-profiles.tsx | 35 + .../components/profile/explore-profiles.tsx | 44 + .../profile/follow-unfollow-button.tsx | 117 + .../components/profile/owned-profiles.tsx | 146 + .../components/profile/profile-card.tsx | 86 + .../components/profile/profile-list-modal.tsx | 105 + .../profile/profile-publications.tsx | 69 + .../components/profile/profile-revenue.tsx | 36 + .../components/profile/profile-stats.tsx | 68 + .../components/profile/profile.tsx | 191 + .../components/profile/search-profiles.tsx | 44 + .../publications/actions/button.tsx | 50 + .../publications/actions/comment.tsx | 30 + .../components/publications/actions/index.tsx | 6 + .../components/publications/actions/like.tsx | 96 + .../publications/actions/mirror.tsx | 98 + .../components/publications/commnets.tsx | 46 + .../publications/explore-publications.tsx | 40 + .../publication-actions-and-stats.tsx | 25 + .../publications/publication-card.tsx | 243 ++ .../publications/publication-revenue.tsx | 29 + .../components/publications/publication.tsx | 115 + .../publications/search-publications.tsx | 32 + .../components/publications/stats/index.tsx | 69 + .../components/publications/stats/stat.tsx | 33 + .../lens-protocol/hooks/use-create-profile.ts | 23 + .../core/lens-protocol/lens-provider.ts | 7 + .../core/lens-protocol/utils/index.ts | 14 + .../pages/lens-protocol/explore/page.tsx | 13 + .../pages/lens-protocol/layout.tsx | 75 + .../pages/lens-protocol/opengraph-image.tsx | 9 + .../pages/lens-protocol/page.tsx | 5 + .../lens-protocol/profiles/[handle]/page.tsx | 11 + .../profiles/address/[address]/page.tsx | 11 + .../pages/lens-protocol/profiles/page.tsx | 7 + .../lens-protocol/publications/[id]/page.tsx | 13 + .../pages/lens-protocol/search/page.tsx | 17 + .../pages/lens-protocol/twitter-image.tsx | 9 + .../api/lit-protocol/[id]/route.ts | 2 +- .../api/lit-protocol/encrypt/route.ts | 2 +- .../core/lit-protocol/api/[id].ts | 19 +- .../core/lit-protocol/api/encrypt.ts | 41 +- .../lit-protocol/core/lit-protocol/client.ts | 2 +- .../access-control-single-address.tsx | 46 +- .../access-control-single-erc721.tsx | 85 +- .../access-control-token-group.tsx | 141 +- .../components/access-control/index.ts | 12 +- .../components/access-control/types.ts | 4 +- .../components/form-lit-decrypt-message.tsx | 126 +- .../components/form-lit-encrypt-message.tsx | 304 +- .../core/lit-protocol/hooks/use-lit-client.ts | 98 +- .../core/lit-protocol/utils/config.ts | 38 +- .../core/lit-protocol/utils/types.ts | 6 +- .../pages/lit-protocol/layout.tsx | 65 - .../pages/lit-protocol/opengraph-image.tsx | 6 +- .../lit-protocol/pages/lit-protocol/page.tsx | 86 + .../pages/lit-protocol/share/page.tsx | 21 - .../pages/lit-protocol/twitter-image.tsx | 4 +- .../pages/lit-protocol/unseal/page.tsx | 29 - .../core/livepeer/components/button-share.tsx | 43 +- .../livepeer/components/create-stream.tsx | 301 +- .../components/dialog-stop-stream.tsx | 32 +- .../components/form-livepeer-api-key.tsx | 93 +- .../components/form-livepeer-asset.tsx | 40 +- .../components/form-livepeer-stream.tsx | 45 +- .../core/livepeer/components/player.tsx | 21 +- .../core/livepeer/components/spinner.tsx | 5 +- .../core/livepeer/components/upload-file.tsx | 78 +- .../hooks/use-check-livepeer-api-key.ts | 14 +- .../livepeer/hooks/use-livepeer-api-key.ts | 5 +- .../livepeer/core/livepeer/livepeer-client.ts | 2 +- .../core/livepeer/livepeer-provider.tsx | 29 +- .../livepeer/pages/livepeer/layout.tsx | 126 +- .../livepeer/livestream/[streamId]/page.tsx | 47 +- .../livepeer/livestream/new/browser/page.tsx | 2 +- .../livepeer/livestream/new/obs/page.tsx | 2 +- .../pages/livepeer/livestream/new/page.tsx | 5 +- .../pages/livepeer/livestream/page.tsx | 66 +- .../pages/livepeer/livestream/watch/page.tsx | 2 +- .../pages/livepeer/opengraph-image.tsx | 6 +- .../livepeer/pages/livepeer/page.tsx | 4 +- .../livepeer/pages/livepeer/twitter-image.tsx | 4 +- .../pages/livepeer/vod/[playbackId]/page.tsx | 46 +- .../livepeer/pages/livepeer/vod/new/page.tsx | 2 +- .../livepeer/pages/livepeer/vod/page.tsx | 50 +- .../pages/livepeer/vod/watch/page.tsx | 2 +- .../moralis/core/moralis/api/events.ts | 34 +- .../moralis/core/moralis/api/transaction.ts | 37 +- .../moralis/core/moralis/client/index.ts | 8 +- .../events/form-get-contract-events.tsx | 100 +- .../events/form-get-contract-logs.tsx | 87 +- .../core/moralis/components/events/index.ts | 4 +- .../core/moralis/components/output-data.tsx | 29 +- .../form-get-internal-transactions.tsx | 86 +- .../form-get-transaction-verbose.tsx | 90 +- .../transaction/form-get-transaction.tsx | 85 +- .../form-get-wallet-transactions-verbose.tsx | 88 +- .../form-get-wallet-transactions.tsx | 89 +- .../moralis/components/transaction/index.ts | 10 +- .../core/moralis/hooks/events/index.ts | 4 +- .../hooks/events/use-get-contract-events.ts | 81 +- .../hooks/events/use-get-contract-logs.ts | 69 +- .../core/moralis/hooks/transaction/index.ts | 10 +- .../use-get-internal-transactions.ts | 23 +- .../use-get-transaction-verbose.ts | 23 +- .../hooks/transaction/use-get-transaction.ts | 31 +- .../use-get-wallet-transactions-verbose.ts | 31 +- .../use-get-wallet-transactions.ts | 31 +- .../moralis/core/moralis/utils/types.ts | 19 +- .../moralis/api/events/[method]/route.ts | 2 +- .../moralis/api/transaction/[method]/route.ts | 2 +- .../moralis/pages/moralis/events/page.tsx | 19 - .../moralis/pages/moralis/layout.tsx | 72 - .../moralis/pages/moralis/opengraph-image.tsx | 6 +- .../moralis/pages/moralis/page.tsx | 96 +- .../pages/moralis/transaction/page.tsx | 28 - .../moralis/pages/moralis/twitter-image.tsx | 4 +- .../openai/api/openai/ask/route.ts | 4 +- .../openai/core/openai/api/ask.ts | 10 +- .../openai/components/form-openai-prompt.tsx | 143 +- .../core/openai/hooks/use-openai-prompt.ts | 12 +- .../openai/core/openai/openai-stream.ts | 32 +- .../openai/core/openai/utils/types.ts | 10 +- .../openai/pages/openai/opengraph-image.tsx | 6 +- .../integrations/openai/pages/openai/page.tsx | 87 +- .../openai/pages/openai/twitter-image.tsx | 4 +- .../abis/yield-source-prize-pool-abi.ts | 774 ++-- .../abis/yield-source-prize-pool-bytecode.ts | 2 +- .../form-yield-source-prize-pool-deposit.tsx | 243 +- .../form-yield-source-prize-pool-withdraw.tsx | 168 +- .../hooks/use-load-contract-from-chain-id.ts | 3 +- .../pooltogether-v4/hooks/use-user-balance.ts | 16 +- .../utils/prize-pool-contract-list.ts | 6 +- .../utils/ticket-contract-list.ts | 6 +- .../utils/usdc-contract-list.ts | 6 +- .../core/pooltogether-v4/wagmi.config.ts | 11 +- .../pages/pooltogether-v4/deposit/page.tsx | 21 - .../pages/pooltogether-v4/layout.tsx | 73 - .../pages/pooltogether-v4/opengraph-image.tsx | 6 +- .../pages/pooltogether-v4/page.tsx | 80 +- .../pages/pooltogether-v4/twitter-image.tsx | 4 +- .../pages/pooltogether-v4/withdraw/page.tsx | 21 - .../push-protocol/components/channel-card.tsx | 73 +- .../components/channel-search.tsx | 55 +- .../core/push-protocol/components/chat.tsx | 2 +- .../core/push-protocol/components/index.ts | 16 +- .../push-protocol/components/loadable.tsx | 13 +- .../components/notification-bell.tsx | 76 +- .../components/notification-feed.tsx | 62 +- .../components/notification-item.tsx | 29 +- .../components/subscribe-button.tsx | 77 +- .../core/push-protocol/hooks/index.ts | 18 +- .../core/push-protocol/hooks/use-channel.ts | 15 +- .../core/push-protocol/hooks/use-chats.ts | 27 +- .../push-protocol/hooks/use-create-user.ts | 6 +- .../push-protocol/hooks/use-notifications.ts | 26 +- .../hooks/use-search-channels.ts | 29 +- .../hooks/use-send-notifications.ts | 4 +- .../hooks/use-subscribe-channel.ts | 6 +- .../hooks/use-unsubscribe-channel.ts | 6 +- .../hooks/use-user-subscriptions.ts | 23 +- .../push-protocol/core/push-protocol/index.ts | 10 +- .../core/push-protocol/utils/constants.ts | 2 +- .../core/push-protocol/utils/helpers.ts | 30 +- .../core/push-protocol/utils/types.ts | 12 +- .../pages/push-protocol/opengraph-image.tsx | 6 +- .../pages/push-protocol/page.tsx | 221 +- .../pages/push-protocol/twitter-image.tsx | 4 +- .../components/create-session-key.tsx | 8 +- .../components/delete-all-session-keys.tsx | 12 +- .../components/delete-session-key.tsx | 20 +- .../components/list-session-keys.tsx | 51 +- .../core/session-keys/database.ts | 8 +- .../session-keys/hooks/use-session-keys.ts | 34 +- .../pages/session-keys/opengraph-image.tsx | 6 +- .../session-keys/pages/session-keys/page.tsx | 119 +- .../pages/session-keys/twitter-image.tsx | 4 +- .../starter/core/starter/abis/starter-abi.ts | 214 +- .../starter/core/starter/client/index.ts | 4 +- .../starter/core/starter/hooks/use-starter.ts | 2 +- .../starter/core/starter/wagmi.config.ts | 10 +- .../pages/starter/api/hello-world/route.ts | 4 +- .../starter/pages/starter/layout-optional.tsx | 50 + .../starter/pages/starter/layout.tsx | 58 - .../starter/pages/starter/opengraph-image.tsx | 6 +- .../starter/pages/starter/page.tsx | 95 +- .../starter/pages/starter/twitter-image.tsx | 4 +- 690 files changed, 29402 insertions(+), 14217 deletions(-) create mode 100644 template/base/.eslintrc.cjs delete mode 100644 template/base/.eslintrc.json rename template/base/{ => .github}/workflows/ci.yml (94%) delete mode 100644 template/base/.prettierrc.json delete mode 100644 template/base/app/(general)/account/page.tsx delete mode 100644 template/base/app/admin/layout.tsx delete mode 100644 template/base/app/admin/page.tsx create mode 100644 template/base/app/dashboard/admin/page.tsx create mode 100644 template/base/app/opengraph-image.png delete mode 100644 template/base/app/twitter-image.tsx delete mode 100644 template/base/components/layout/dashboard-footer.tsx delete mode 100644 template/base/components/layout/dashboard-header.tsx delete mode 100644 template/base/components/layout/header.tsx create mode 100644 template/base/components/layout/main-nav.tsx delete mode 100644 template/base/components/layout/menu-admin-sidebar.tsx delete mode 100644 template/base/components/layout/menu-dashboard-sidebar.tsx create mode 100644 template/base/components/layout/mobile-nav.tsx delete mode 100644 template/base/components/layout/navigation-menu-general.tsx create mode 100644 template/base/components/layout/page-header.tsx create mode 100644 template/base/components/layout/page-section.tsx create mode 100644 template/base/components/layout/sidebar-nav.tsx create mode 100644 template/base/components/layout/site-header.tsx delete mode 100644 template/base/components/layout/user-dropdown.tsx delete mode 100644 template/base/components/shared/card.tsx create mode 100644 template/base/components/shared/copy-button.tsx create mode 100644 template/base/components/shared/example-demos.tsx delete mode 100644 template/base/components/shared/icons.tsx delete mode 100644 template/base/components/shared/is-desktop.tsx delete mode 100644 template/base/components/shared/is-mobile.tsx create mode 100644 template/base/components/shared/light-dark-image.tsx create mode 100644 template/base/components/shared/mode-toggle.tsx delete mode 100644 template/base/components/shared/theme-toggle.tsx delete mode 100644 template/base/components/ui/aspect-ratio.tsx create mode 100644 template/base/components/ui/badge.tsx create mode 100644 template/base/components/ui/card.tsx delete mode 100644 template/base/components/ui/collapsible.tsx delete mode 100644 template/base/components/ui/context-menu.tsx delete mode 100644 template/base/components/ui/hover-card.tsx delete mode 100644 template/base/components/ui/menubar.tsx delete mode 100644 template/base/components/ui/radio-group.tsx create mode 100644 template/base/components/ui/sheet.tsx create mode 100644 template/base/components/ui/skeleton.tsx delete mode 100644 template/base/components/ui/slider.tsx create mode 100644 template/base/components/ui/use-toast.tsx delete mode 100644 template/base/integrations/siwe/components/branch-button-login-or-account.tsx create mode 100644 template/base/lib/fonts.ts create mode 100644 template/base/lib/utils/motion.ts mode change 100755 => 100644 template/base/public/favicon.ico create mode 100644 template/base/public/icon-192x192.png create mode 100644 template/base/public/icon-256x256.png create mode 100644 template/base/public/icon-384x384.png create mode 100644 template/base/public/icon-512x512.png delete mode 100644 template/base/public/integrations/disco.jpeg create mode 100644 template/base/public/integrations/discoDark.png rename template/base/public/integrations/{disco.png => discoLight.png} (100%) create mode 100644 template/base/public/integrations/erc1155-icon.png create mode 100644 template/base/public/integrations/erc20.png create mode 100644 template/base/public/integrations/gitcoin-passport.svg create mode 100644 template/base/public/integrations/lensprotocol-dark.svg create mode 100644 template/base/public/integrations/lensprotocol-light.svg delete mode 100644 template/base/public/logo-fill.png create mode 100644 template/base/public/logo-light.png delete mode 100644 template/base/public/logo-white.png delete mode 100644 template/base/public/logo.png create mode 100644 template/base/public/manifest.json delete mode 100644 template/base/public/preview.png create mode 100644 template/base/styles/globals.css delete mode 100644 template/base/styles/gradient.css delete mode 100644 template/base/styles/periphery.css delete mode 100644 template/integrations/aave/pages/aave/layout.tsx delete mode 100644 template/integrations/connext/pages/connext/layout.tsx delete mode 100644 template/integrations/disco/pages/disco/layout.tsx delete mode 100644 template/integrations/disco/pages/disco/profile/page.tsx delete mode 100644 template/integrations/disco/pages/disco/proof-of-hack/page.tsx create mode 100644 template/integrations/erc1155/core/erc1155/README.md create mode 100644 template/integrations/erc1155/core/erc1155/artifacts/core/erc1155-abi.ts create mode 100644 template/integrations/erc1155/core/erc1155/artifacts/core/erc1155-bytecode.ts create mode 100644 template/integrations/erc1155/core/erc1155/artifacts/test/erc1155-abi.ts create mode 100644 template/integrations/erc1155/core/erc1155/artifacts/test/erc1155-bytecode.ts create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-contract-uri.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-deploy-test.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-deploy.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-name.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-owner-of.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-read.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-set-token-storage.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-symbol.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-token-total-supply.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-token-uri-description.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-token-uri-image.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-token-uri-name.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-token-uri-symbol.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-token-uri.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-write-approve-for-all.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-write-approve.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-write-batch-transfer.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-write-mint.tsx create mode 100644 template/integrations/erc1155/core/erc1155/components/erc1155-write-transfer.tsx create mode 100644 template/integrations/erc1155/core/erc1155/generated/erc1155-wagmi.ts create mode 100644 template/integrations/erc1155/core/erc1155/hooks/use-erc1155-metadata.ts create mode 100644 template/integrations/erc1155/core/erc1155/hooks/use-erc1155-token-storage.ts create mode 100644 template/integrations/erc1155/core/erc1155/index.ts create mode 100644 template/integrations/erc1155/core/erc1155/utils/types.ts create mode 100644 template/integrations/erc1155/core/erc1155/wagmi.config.ts create mode 100644 template/integrations/erc1155/pages/erc1155/opengraph-image.tsx create mode 100644 template/integrations/erc1155/pages/erc1155/page.tsx create mode 100644 template/integrations/erc1155/pages/erc1155/twitter-image.tsx delete mode 100644 template/integrations/gelato/pages/gelato/layout.tsx create mode 100644 template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/score/route.ts create mode 100644 template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/stamps/route.ts create mode 100644 template/integrations/gitcoin-passport/api/gitcoin-passport/signing-message/route.ts create mode 100644 template/integrations/gitcoin-passport/api/gitcoin-passport/stamps-metadata/route.ts create mode 100644 template/integrations/gitcoin-passport/api/gitcoin-passport/submit-passport/route.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/README.md create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-score.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-stamps.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/api/signing-message.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/api/stamps-metadata.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/api/submit-passport.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/components/list-stamps.tsx create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/components/score-gate.tsx create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-card.tsx create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-gate.tsx create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/components/submit-passport-button.tsx create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-address-stamps.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-score.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-stamps-metadata.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-submit-passport.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/utils/constants.ts create mode 100644 template/integrations/gitcoin-passport/core/gitcoin-passport/utils/types.ts create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/dev-guide/page.tsx create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/layout.tsx create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/opengraph-image.tsx create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/page.tsx create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/score-gated/page.tsx create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/sidebar.tsx create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/stamp-gated/page.tsx create mode 100644 template/integrations/gitcoin-passport/pages/gitcoin-passport/twitter-image.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/README.md create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/auth/is-user-authenticated.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/auth/login-button.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/auth/logout-button.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/auth/not-authenticated-yet.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/feed.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/load-more-button.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/navbar.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/address-profiles.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/explore-profiles.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/follow-unfollow-button.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/owned-profiles.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-card.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-list-modal.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-publications.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-revenue.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-stats.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/profile.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/profile/search-profiles.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/button.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/comment.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/index.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/like.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/mirror.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/commnets.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/explore-publications.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-actions-and-stats.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-card.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-revenue.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/publication.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/search-publications.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/index.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/stat.tsx create mode 100644 template/integrations/lens-protocol/core/lens-protocol/hooks/use-create-profile.ts create mode 100644 template/integrations/lens-protocol/core/lens-protocol/lens-provider.ts create mode 100644 template/integrations/lens-protocol/core/lens-protocol/utils/index.ts create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/explore/page.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/layout.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/opengraph-image.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/page.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/profiles/[handle]/page.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/profiles/address/[address]/page.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/profiles/page.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/publications/[id]/page.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/search/page.tsx create mode 100644 template/integrations/lens-protocol/pages/lens-protocol/twitter-image.tsx delete mode 100644 template/integrations/lit-protocol/pages/lit-protocol/layout.tsx create mode 100644 template/integrations/lit-protocol/pages/lit-protocol/page.tsx delete mode 100644 template/integrations/lit-protocol/pages/lit-protocol/share/page.tsx delete mode 100644 template/integrations/lit-protocol/pages/lit-protocol/unseal/page.tsx delete mode 100644 template/integrations/moralis/pages/moralis/events/page.tsx delete mode 100644 template/integrations/moralis/pages/moralis/layout.tsx delete mode 100644 template/integrations/moralis/pages/moralis/transaction/page.tsx delete mode 100644 template/integrations/pooltogether-v4/pages/pooltogether-v4/deposit/page.tsx delete mode 100644 template/integrations/pooltogether-v4/pages/pooltogether-v4/layout.tsx delete mode 100644 template/integrations/pooltogether-v4/pages/pooltogether-v4/withdraw/page.tsx create mode 100644 template/integrations/starter/pages/starter/layout-optional.tsx delete mode 100644 template/integrations/starter/pages/starter/layout.tsx diff --git a/.gitignore b/.gitignore index 406f95a..c5fc356 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,4 @@ /tmp node_modules oclif.manifest.json -.github .vscode \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts index faf23d2..7fc58b4 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -134,7 +134,7 @@ export default class Core extends Command { ux.action.start('Installing packages. This might take a few minutes') if (!skipInstall) { - const installArgs = ['install', packageManager === 'npm' ? '--quiet' : '--silent', '--legacy-peer-deps'] + const installArgs = ['install', packageManager === 'npm' ? '--quiet --legacy-peer-deps' : '--silent'] try { await execa(packageManager, installArgs, { cwd: projectDir, env: { ...process.env, NODE_ENV: 'development' } }) } catch (error) { diff --git a/src/config/integrations.ts b/src/config/integrations.ts index bd46e1f..ffc21c5 100644 --- a/src/config/integrations.ts +++ b/src/config/integrations.ts @@ -2,7 +2,7 @@ import type { Integrations } from '../types' import path from 'node:path' import { z } from 'zod' -const indexPagePath = path.join('app', '(general)', 'page.tsx') +const exampleDemosPath = path.join('components', 'shared', 'example-demos.tsx') const dataConfigPath = path.join('data', 'turbo-integrations.ts') export const integrationOptions: Integrations = { @@ -12,14 +12,14 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*erc20: {\s*name: 'ERC20',[\S\s]*?imgDark: '\/integrations\/erc20-icon\.png',\s*},/g], + regexList: [/\n\s*erc20: {\s*name: "ERC20",[\S\s]*?imgDark: "\/integrations\/erc20\.png",\s*},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [ - /\nimport \{ ERC20Decimals, ERC20Name, ERC20Symbol \} from '@\/integrations\/erc20\/components\/erc20-read'/g, - /\n\s*{\s*title: 'ERC20 WAGMI',[\s\S]*?<\/LinkComponent>\s*<\/div>\s*\),\s*},/g, + /\nimport \{\n {2}ERC20Decimals,\n {2}ERC20Name,\n {2}ERC20Symbol,\n\} from "@\/integrations\/erc20\/components\/erc20-read"/g, + /\n\s*{\s*title: "ERC20 WAGMI",[\s\S]*?<\/Link>\s*<\/div>\s*\),\s*},/g, ], }, ], @@ -30,28 +30,38 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*erc721: \{\s*name: 'ERC721',[\s\S]*?imgDark: '\/integrations\/erc721-icon.png',\s*\},/g], + regexList: [/\n\s*erc721: \{\s*name: "ERC721",[\s\S]*?imgDark: "\/integrations\/erc721-icon.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [ - /\nimport { ERC721TokenUriImage, ERC721TokenUriName } from '@\/integrations\/erc721'/g, - /\n\s*{\s*title: 'ERC721 WAGMI',[\s\S]*?<\/LinkComponent>\s*<\/div>\s*\),\s*},/g, + /\nimport { ERC721TokenUriImage, ERC721TokenUriName } from "@\/integrations\/erc721"/g, + /\n\s*{\s*title: "ERC721 WAGMI",[\s\S]*?<\/Link>\s*<\/div>\s*\),\s*},/g, ], }, ], }, + erc1155: { + name: 'ERC1155', + pageDependencies: [ + { + dependencyPath: dataConfigPath, + type: 'snippet', + regexList: [/\n\s*erc1155: {\s*name: "ERC1155",[\S\s]*?imgDark: "\/integrations\/erc1155-icon\.png",\s*},/g], + }, + ], + }, disco: { name: 'Disco', pageDependencies: [ { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*disco: \{\s*name: 'Disco',[\s\S]*?imgDark: '\/integrations\/disco.jpeg',\s*\},/g], + regexList: [/\n\s*disco: \{\s*name: "Disco",[\s\S]*?imgDark: "\/integrations\/discoDark\.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.disco\.name,[\S\s]*?href: turboIntegrations\.disco\.href,[\S\s]*?<\/div>\s*\),\s*},/g], }, @@ -76,10 +86,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*etherscan: \{\s*name: 'Etherscan',[\s\S]*?imgDark: '\/integrations\/etherscan-dark.svg',\s*\},/g], + regexList: [/\n\s*etherscan: \{\s*name: "Etherscan",[\s\S]*?imgDark: "\/integrations\/etherscan-dark.svg",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.etherscan\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -90,7 +100,7 @@ export const integrationOptions: Integrations = { { dependencyPath: path.join('config', 'menu-dashboard.ts'), type: 'snippet', - regexList: [/\n\s*{\s*label: 'Transactions',[\s\S]*?},/g], + regexList: [/\n\s*{\s*label: "Transactions",[\s\S]*?},/g], }, ], // Don't require Etherscan API keys since they are not required to make API requests @@ -119,10 +129,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*litProtocol: \{\s*name: 'Lit Protocol',[\s\S]*?imgDark: '\/integrations\/lit-protocol.png',\s*\},/g], + regexList: [/\n\s*litProtocol: \{\s*name: "Lit Protocol",[\s\S]*?imgDark: "\/integrations\/lit-protocol.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.litProtocol\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -134,10 +144,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*openai: \{\s*name: 'OpenAI',[\s\S]*?imgDark: '\/integrations\/openai-dark.svg',\s*\},/g], + regexList: [/\n\s*openai: \{\s*name: "OpenAI",[\s\S]*?imgDark: "\/integrations\/openai-dark.svg",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.openai\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -162,10 +172,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*pooltogether_v4: \{\s*name: 'PoolTogether',[\s\S]*?imgDark: '\/integrations\/pooltogether.svg',\s*\},/g], + regexList: [/\n\s*pooltogether_v4: \{\s*name: "PoolTogether",[\s\S]*?imgDark: "\/integrations\/pooltogether.svg",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.pooltogether_v4\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -177,10 +187,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*sessionKeys: \{\s*name: 'Session Keys',[\s\S]*?imgDark: '\/integrations\/session-keys.png',\s*\},/g], + regexList: [/\n\s*sessionKeys: \{\s*name: "Session Keys",[\s\S]*?imgDark: "\/integrations\/session-keys.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.sessionKeys\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -192,10 +202,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*connext: \{\s*name: 'Connext',[\s\S]*?imgDark: '\/integrations\/connext.png',\s*\},/g], + regexList: [/\n\s*connext: \{\s*name: "Connext",[\s\S]*?imgDark: "\/integrations\/connext.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.connext\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -207,10 +217,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*livepeer: \{\s*name: 'Livepeer',[\s\S]*?imgDark: '\/integrations\/livepeer.svg',\s*\},/g], + regexList: [/\n\s*livepeer: \{\s*name: "Livepeer",[\s\S]*?imgDark: "\/integrations\/livepeer.svg",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.livepeer\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -235,10 +245,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*gelato: \{\s*name: 'Gelato',[\s\S]*?imgDark: '\/integrations\/gelato-light.svg',\s*\},/g], + regexList: [/\n\s*gelato: \{\s*name: "Gelato",[\s\S]*?imgDark: "\/integrations\/gelato-light.svg",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.gelato\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -250,10 +260,10 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*push_protocol: \{\s*name: 'Push Protocol',[\s\S]*?imgDark: '\/integrations\/push.svg',\s*\},/g], + regexList: [/\n\s*push_protocol: \{\s*name: "Push Protocol",[\s\S]*?imgDark: "\/integrations\/push.svg",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', regexList: [/\n\s*{\s*title: turboIntegrations\.push_protocol\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], }, @@ -265,12 +275,12 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*moralis: \{\s*name: 'Moralis',[\s\S]*?imgDark: '\/integrations\/moralis.png',\s*\},/g], + regexList: [/\n\s*moralis: \{\s*name: "Moralis",[\s\S]*?imgDark: "\/integrations\/moralis.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', - regexList: [/\n\s*{\s*title: turboIntegrations\.moralis\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], + regexList: [/\n\s*{\s*title: turboIntegrations\.moralis\.name,[\s\S]*?\/>\s*<\/div>\s*\),\s*},/g], }, ], env: { @@ -293,12 +303,12 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*aave: \{\s*name: 'Aave',[\s\S]*?imgDark: '\/integrations\/aave.png',\s*\},/g], + regexList: [/\n\s*aave: \{\s*name: "Aave",[\s\S]*?imgDark: "\/integrations\/aave.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', - regexList: [/\n\s*{\s*title: turboIntegrations\.aave\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], + regexList: [/\n\s*{\s*title: turboIntegrations\.aave\.name,[\s\S]*?\/>\s*<\/div>\s*\),\s*},/g], }, ], }, @@ -308,12 +318,42 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*arweave: \{\s*name: 'Arweave',[\s\S]*?imgDark: '\/integrations\/arweave-dark.png',\s*\},/g], + regexList: [/\n\s*arweave: \{\s*name: "Arweave",[\s\S]*?imgDark: "\/integrations\/arweave-dark.png",\s*\},/g], + }, + { + dependencyPath: exampleDemosPath, + type: 'snippet', + regexList: [/\n\s*{\s*title: turboIntegrations\.arweave\.name,[\s\S]*?\/>\s*<\/div>\s*\),\s*},/g], + }, + ], + }, + 'gitcoin-passport': { + name: 'Gitcoin Passport', + pageDependencies: [ + { + dependencyPath: dataConfigPath, + type: 'snippet', + regexList: [/\n\s*gitcoinPassport: \{\s*name: "Gitcoin Passport",[\s\S]*?imgDark: "\/integrations\/gitcoin-passport.svg",\s*\},/g], + }, + { + dependencyPath: exampleDemosPath, + type: 'snippet', + regexList: [/\n\s*{\s*title: turboIntegrations\.gitcoinPassport\.name,[\s\S]*?\/>\s*<\/div>\s*\),\s*},/g], + }, + ], + }, + 'lens-protocol': { + name: 'Lens Protocol', + pageDependencies: [ + { + dependencyPath: dataConfigPath, + type: 'snippet', + regexList: [/\n\s*lensProtocol: \{\s*name: "Lens Protocol",[\s\S]*?imgDark: "\/integrations\/lensprotocol-dark.svg",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', - regexList: [/\n\s*{\s*title: turboIntegrations\.arweave\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], + regexList: [/\n\s*{\s*title: turboIntegrations\.lensProtocol\.name,[\s\S]*?\/>\s*<\/div>\s*\),\s*},/g], }, ], }, @@ -323,12 +363,12 @@ export const integrationOptions: Integrations = { { dependencyPath: dataConfigPath, type: 'snippet', - regexList: [/\n\s*starter: \{\s*name: 'Starter Template',[\s\S]*?imgDark: '\/logo-dark.png',\s*\},/g], + regexList: [/\n\s*starter: \{\s*name: "Starter Template",[\s\S]*?imgDark: "\/logo-gradient\.png",\s*\},/g], }, { - dependencyPath: indexPagePath, + dependencyPath: exampleDemosPath, type: 'snippet', - regexList: [/\n\s*{\s*title: turboIntegrations\.starter\.name,[\s\S]*?<\/IsDarkTheme>\s*<\/div>\s*\),\s*},/g], + regexList: [/\n\s*{\s*title: turboIntegrations\.starter\.name,[\s\S]*?\/>\s*<\/div>\s*\),\s*},/g], }, ], }, diff --git a/src/types.ts b/src/types.ts index c88e2cc..78c75e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,6 +61,7 @@ export type EnvVariable = { export type AvailableIntegrations = | 'erc20' + | 'erc1155' | 'erc721' | 'disco' | 'etherscan' @@ -75,6 +76,8 @@ export type AvailableIntegrations = | 'moralis' | 'aave' | 'arweave' + | 'gitcoin-passport' + | 'lens-protocol' | 'starter' export type Integrations = Record diff --git a/template/base/.env.example b/template/base/.env.example index ec828e4..ddbd65e 100644 --- a/template/base/.env.example +++ b/template/base/.env.example @@ -47,3 +47,7 @@ OPENAI_API_KEY= # Moralis API Key: https://admin.moralis.io/settings#secret-keys MORALIS_API_KEY= + +# Gitcoin Passport Scorer ID and API key: https://scorer.gitcoin.co/#/dashboard/scorer +GITCOIN_PASSPORT_SCORER_ID= +GITCOIN_PASSPORT_API_KEY= \ No newline at end of file diff --git a/template/base/.eslintignore b/template/base/.eslintignore index fbffc8a..0f2eff1 100644 --- a/template/base/.eslintignore +++ b/template/base/.eslintignore @@ -3,3 +3,4 @@ **/components/shared/table/** **/generated .next/** +tailwind.config.js \ No newline at end of file diff --git a/template/base/.eslintrc.cjs b/template/base/.eslintrc.cjs new file mode 100644 index 0000000..eb8de00 --- /dev/null +++ b/template/base/.eslintrc.cjs @@ -0,0 +1,38 @@ +/* eslint-env node */ +module.exports = { + root: true, + extends: [ + "next/core-web-vitals", + "eslint:recommended", + "prettier", + "plugin:tailwindcss/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + plugins: ["@typescript-eslint", "tailwindcss"], + parser: "@typescript-eslint/parser", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + }, + rules: { + "@next/next/no-html-link-for-pages": "off", + "@next/next/no-img-element": "off", // We currently not using next/image because it isn't supported with SSG mode + "react-hooks/exhaustive-deps": "off", // Incorrectly report needed dependency with Next.js router + "tailwindcss/no-custom-classname": "error", + "tailwindcss/classnames-order": "error", + "@typescript-eslint/no-misused-promises": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "no-unused-vars": "off", + }, + settings: { + tailwindcss: { + callees: ["cn"], + config: "tailwind.config.js", + }, + next: { + rootDir: true, + }, + }, +} diff --git a/template/base/.eslintrc.json b/template/base/.eslintrc.json deleted file mode 100644 index b7cc234..0000000 --- a/template/base/.eslintrc.json +++ /dev/null @@ -1,77 +0,0 @@ -{ - "plugins": ["unused-imports", "tailwindcss", "simple-import-sort", "@typescript-eslint"], - "extends": [ - "plugin:tailwindcss/recommended", - "next/core-web-vitals", - "plugin:prettier/recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig.json" - }, - "rules": { - "prettier/prettier": [ - "error", - { - "singleQuote": true, - "endOfLine": "auto" - } - ], - "react/destructuring-assignment": "off", // Vscode doesn't support automatically destructuring, it's a pain to add a new variable - "jsx-a11y/anchor-is-valid": "off", // Next.js use his own internal link system - "react/require-default-props": "off", // Allow non-defined react props as undefined - "react/jsx-props-no-spreading": "off", // _app.tsx uses spread operator and also, react-hook-form - "react-hooks/exhaustive-deps": "off", // Incorrectly report needed dependency with Next.js router - "@next/next/no-img-element": "off", // We currently not using next/image because it isn't supported with SSG mode - "@typescript-eslint/comma-dangle": "off", // Avoid conflict rule between Eslint and Prettier - "@typescript-eslint/consistent-type-imports": "off", // Ensure `import type` is used when it's necessary - "import/prefer-default-export": "off", // Named export is easier to refactor automatically - "unused-imports/no-unused-vars": "off", - "@typescript-eslint/require-await": "off", - "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/no-unsafe-assignment": "off", - "react/jsx-sort-props": [ - "error", - { - "callbacksLast": true, - "shorthandFirst": true, - "ignoreCase": true, - "reservedFirst": true, - "multiline": "last" - } - ], - "tailwindcss/classnames-order": [ - "warn", - { - "officialSorting": true - } - ], - "sort-imports": [ - 1, - { - "ignoreDeclarationSort": true - } - ], - "import/order": [ - 1, - { - "groups": ["builtin", "external", "internal"], - "pathGroups": [ - { - "pattern": "react", - "group": "external", - "position": "before" - } - ], - "pathGroupsExcludedImportTypes": [""], - "newlines-between": "always", - "alphabetize": { - "order": "asc", - "caseInsensitive": true - } - } - ] - } -} diff --git a/template/base/workflows/ci.yml b/template/base/.github/workflows/ci.yml similarity index 94% rename from template/base/workflows/ci.yml rename to template/base/.github/workflows/ci.yml index 3b5987d..52cf51b 100644 --- a/template/base/workflows/ci.yml +++ b/template/base/.github/workflows/ci.yml @@ -1,6 +1,6 @@ name: CI -on: +on: push: branches: - main @@ -38,12 +38,10 @@ jobs: path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-pnpm-store- + ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install --frozen-lockfile - name: Check formatting and linting run: pnpm run-ci - - \ No newline at end of file diff --git a/template/base/.prettierignore b/template/base/.prettierignore index 7ac7fc1..73bfe33 100644 --- a/template/base/.prettierignore +++ b/template/base/.prettierignore @@ -1,10 +1,8 @@ +dist +node_modules +.next +build +pnpm-lock.yaml **/generated/**-wagmi.ts **/generated/blockchain.ts **/generated/ - -.next/** -.husky/** -.vscode/** -.github/** - -pnpm-lock.yaml diff --git a/template/base/.prettierrc.json b/template/base/.prettierrc.json deleted file mode 100644 index dee3587..0000000 --- a/template/base/.prettierrc.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "trailingComma": "es5", - "semi": false, - "singleQuote": true, - "printWidth": 150, - "bracketSameLine": true, - "useTabs": false, - "tabWidth": 2 -} diff --git a/template/base/.vscode/settings.json b/template/base/.vscode/settings.json index fd897a0..f11832e 100644 --- a/template/base/.vscode/settings.json +++ b/template/base/.vscode/settings.json @@ -10,16 +10,10 @@ "source.addMissingImports", "source.fixAll.eslint" ], - "eslint.validate": [ - "typescript", - "javascript", - "javascriptreact" - ], + "eslint.validate": ["typescript", "javascript", "javascriptreact"], "jest.autoRun": { "watch": true, // Start the jest with the watch flag - "onStartup": [ - "all-tests" - ] // Run all tests upon project launch + "onStartup": ["all-tests"] // Run all tests upon project launch }, "jest.showCoverageOnLoad": true, // Show code coverage when the project is launched "jest.showTerminalOnLaunch": false, // Don't automatically open test explorer terminal on launch @@ -33,5 +27,6 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "typescript.tsdk": "node_modules/.pnpm/typescript@4.9.4/node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true -} \ No newline at end of file + "typescript.enablePromptUseWorkspaceTsdk": true, + "prettier.prettierPath": "./node_modules/prettier" // Workaround fix for prettier force checking node modules. +} diff --git a/template/base/README.md b/template/base/README.md index 2834a35..987f564 100644 --- a/template/base/README.md +++ b/template/base/README.md @@ -2,16 +2,13 @@ # ⚡ TurboETH - Web3 App Starter Kit -Web3 App Template built using Next.js, RainbowKit, SIWE, Disco, and more! +![CI](https://github.com/turbo-eth/template-web3-app/actions/workflows/ci.yml/badge.svg) +![TS](https://badgen.net/badge/-/TypeScript?icon=typescript&label&labelColor=blue&color=555555) +[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](http://perso.crans.org/besson/LICENSE.html) -### Starter Kit Examples +Web3 App Template built using Next.js, RainbowKit, Tailwind, Sign-In With Ethereum, and more. -- [Main](https://light.turboeth.xyz) - `main` branch -- [Integrations](https://turboeth.xyz) - `integrations` branch - -Deploy TurboETH `main` directly to [Vercel](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app&project-name=TurboETH&repository-name=turbo-eth&demo-title=TurboETH&env=APP_ADMINS,NEXT_PUBLIC_ALCHEMY_API_KEY,NEXTAUTH_SECRET,ETHERSCAN_API_KEY,ETHERSCAN_API_KEY_OPTIMISM,ETHERSCAN_API_KEY_ARBITRUM,ETHERSCAN_API_KEY_POLYGON,DATABASE_URL&envDescription=How%20to%20get%20these%20env%20variables%3A&envLink=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app%2Fblob%2Fmain%2F.env.example) - -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app&project-name=TurboETH&repository-name=turbo-eth&demo-title=TurboETH&env=NEXTAUTH_SECRET,DATABASE_URL&envDescription=How%20to%20get%20these%20env%20variables%3A&envLink=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app%2Fblob%2Fmain%2F.env.example) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app&project-name=TurboETH&repository-name=turbo-eth&demo-title=TurboETH&env=NEXTAUTH_SECRET,DATABASE_URL&envDescription=How%20to%20get%20these%20env%20variables%3A&envLink=https%3A%2F%2Fgithub.com%2Fturbo-eth%2Ftemplate-web3-app%2Fblob%integrations%2F.env.example) ### [Documentation](https://docs.turboeth.xyz) diff --git a/template/base/app/(general)/account/page.tsx b/template/base/app/(general)/account/page.tsx deleted file mode 100644 index c9020f9..0000000 --- a/template/base/app/(general)/account/page.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client' -import { motion } from 'framer-motion' - -import { WalletAddress } from '@/components/blockchain/wallet-address' -import { WalletBalance } from '@/components/blockchain/wallet-balance' -import { WalletNonce } from '@/components/blockchain/wallet-nonce' -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' - -export default function PageDashboardAccount() { - return ( - <> - - -
-

Account

-
-
- Address: -
-
- Balance: -
-
- Nonce: -
-
-
-
- -

Connect Wallet to view your personalized dashboard.

-
-
- - ) -} diff --git a/template/base/app/(general)/integration/sign-in-with-ethereum/opengraph-image.tsx b/template/base/app/(general)/integration/sign-in-with-ethereum/opengraph-image.tsx index 69231d5..0c48945 100644 --- a/template/base/app/(general)/integration/sign-in-with-ethereum/opengraph-image.tsx +++ b/template/base/app/(general)/integration/sign-in-with-ethereum/opengraph-image.tsx @@ -1,9 +1,9 @@ -import { IntegrationOgImage } from '@/components/ui/social/og-image-integrations' +import { IntegrationOgImage } from "@/components/ui/social/og-image-integrations" -export const runtime = 'edge' +export const runtime = "edge" export const size = { width: 1200, height: 630, } -export default IntegrationOgImage('siwe') +export default IntegrationOgImage("siwe") diff --git a/template/base/app/(general)/integration/sign-in-with-ethereum/page.tsx b/template/base/app/(general)/integration/sign-in-with-ethereum/page.tsx index 4659a43..0ac276e 100644 --- a/template/base/app/(general)/integration/sign-in-with-ethereum/page.tsx +++ b/template/base/app/(general)/integration/sign-in-with-ethereum/page.tsx @@ -1,72 +1,66 @@ -'use client' -import { motion } from 'framer-motion' -import Image from 'next/image' -import Balancer from 'react-wrap-balancer' +import Link from "next/link" +import { turboIntegrations } from "@/data/turbo-integrations" +import { LuBook } from "react-icons/lu" -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import { IsDarkTheme } from '@/components/shared/is-dark-theme' -import { IsLightTheme } from '@/components/shared/is-light-theme' -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { LinkComponent } from '@/components/shared/link-component' -import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' -import { turboIntegrations } from '@/data/turbo-integrations' -import { ButtonSIWELogin } from '@/integrations/siwe/components/button-siwe-login' -import { ButtonSIWELogout } from '@/integrations/siwe/components/button-siwe-logout' -import { IsSignedIn } from '@/integrations/siwe/components/is-signed-in' -import { IsSignedOut } from '@/integrations/siwe/components/is-signed-out' +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { + PageHeader, + PageHeaderCTA, + PageHeaderDescription, + PageHeaderHeading, +} from "@/components/layout/page-header" +import { PageSection } from "@/components/layout/page-section" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { LightDarkImage } from "@/components/shared/light-dark-image" +import { ButtonSIWELogin } from "@/integrations/siwe/components/button-siwe-login" +import { ButtonSIWELogout } from "@/integrations/siwe/components/button-siwe-logout" +import { IsSignedIn } from "@/integrations/siwe/components/is-signed-in" +import { IsSignedOut } from "@/integrations/siwe/components/is-signed-out" -export default function PageIntegration() { +export default function SIWEPage() { return ( -
- - - Sign-In With Ethereum logo - - - Sign-In With Ethereum logo - - - {turboIntegrations.siwe.name} - - - {turboIntegrations.siwe.description} - - - +
+ + + SIWE + + Sign-In with Ethereum is Web3 authentication using an Ethereum + account. + + + + Documentation - - - - -
+ + + + - + - + -
+
) } diff --git a/template/base/app/(general)/integration/sign-in-with-ethereum/twitter-image.tsx b/template/base/app/(general)/integration/sign-in-with-ethereum/twitter-image.tsx index dbca541..215a2a5 100644 --- a/template/base/app/(general)/integration/sign-in-with-ethereum/twitter-image.tsx +++ b/template/base/app/(general)/integration/sign-in-with-ethereum/twitter-image.tsx @@ -1,6 +1,6 @@ -import Image from './opengraph-image' +import Image from "./opengraph-image" -export const runtime = 'edge' +export const runtime = "edge" export const size = { width: 1200, height: 630, diff --git a/template/base/app/(general)/layout.tsx b/template/base/app/(general)/layout.tsx index 48f881d..1dc4828 100644 --- a/template/base/app/(general)/layout.tsx +++ b/template/base/app/(general)/layout.tsx @@ -1,27 +1,26 @@ -import { ReactNode } from 'react' +import { ReactNode } from "react" -import { NetworkStatus } from '@/components/blockchain/network-status' -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import { Footer } from '@/components/layout/footer' -import { Header } from '@/components/layout/header' -import { Toaster } from '@/components/ui/toaster' +import { NetworkStatus } from "@/components/blockchain/network-status" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { Footer } from "@/components/layout/footer" +import { SiteHeader } from "@/components/layout/site-header" -export default function GeneralLayout({ children }: { children: ReactNode }) { +interface RootLayoutProps { + children: ReactNode +} + +export default function RootLayout({ children }: RootLayoutProps) { return ( <> -
-
-
{children}
-
- -
-
- -
+
+ +
{children}
- {/* TODO: Add position controls */} - + +
+ +
) } diff --git a/template/base/app/(general)/page.tsx b/template/base/app/(general)/page.tsx index d60128b..6a8c505 100644 --- a/template/base/app/(general)/page.tsx +++ b/template/base/app/(general)/page.tsx @@ -1,427 +1,74 @@ -'use client' +import Image from "next/image" +import Link from "next/link" +import { FaDiscord, FaGithub } from "react-icons/fa" +import { LuBook } from "react-icons/lu" -import { useState } from 'react' - -import { motion } from 'framer-motion' -import Image from 'next/image' -import CopyToClipboard from 'react-copy-to-clipboard' -import { FaCheck, FaCopy, FaDiscord, FaGithub } from 'react-icons/fa' -import Balancer from 'react-wrap-balancer' - -import { WalletAddress } from '@/components/blockchain/wallet-address' -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import Card from '@/components/shared/card' -import { IsDarkTheme } from '@/components/shared/is-dark-theme' -import { IsLightTheme } from '@/components/shared/is-light-theme' -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { LinkComponent } from '@/components/shared/link-component' -import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' -import { DEPLOY_URL, siteConfig } from '@/config/site' -import { turboIntegrations } from '@/data/turbo-integrations' -import { ERC20Decimals, ERC20Name, ERC20Symbol } from '@/integrations/erc20/components/erc20-read' -import { ERC721TokenUriImage, ERC721TokenUriName } from '@/integrations/erc721' -import { ButtonSIWELogin } from '@/integrations/siwe/components/button-siwe-login' -import { ButtonSIWELogout } from '@/integrations/siwe/components/button-siwe-logout' -import { IsSignedIn } from '@/integrations/siwe/components/is-signed-in' -import { IsSignedOut } from '@/integrations/siwe/components/is-signed-out' - -export default function Home() { - const [copied, setCopied] = useState(false) +import { siteConfig } from "@/config/site" +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { + PageHeader, + PageHeaderCTA, + PageHeaderDescription, + PageHeaderHeading, +} from "@/components/layout/page-header" +import { CopyButton } from "@/components/shared/copy-button" +import { ExampleDemos } from "@/components/shared/example-demos" +export default function HomePage() { return ( - <> -
-
- - Turbo ETH - - Build Web3 in Turbo Mode - - - {siteConfig.description} - - - - -

Star on GitHub

-
- - -

Join us on Discord

-
-
- setCopied(true)}> - -
pnpm create turbo-eth@latest
- - {copied ? : } - -
-
-
-
- - {features.map(({ ...props }) => ( - - ))} - -
-
-
- +
+ + TurboETH Logo + Build Web3 in Turbo Mode + {siteConfig.description} + + + + Docs + + + + Github + + + + Discord + + + + + + pnpm create turbo-eth@latest + + + + + +
) } - -const features = [ - { - title: 'Web3 Components for the power developer', - description: 'Pre-built Web3 components, powered by WAGMI', - large: true, - demo: ( -
- -
-
- - <WalletAddress isLink truncate /> -
-
-
- - - -
- ), - }, - { - title: 'One-click Deploy', - description: 'Start your next Web3 project in ⚡ Turbo Mode with a deploy to [Vercel](https://vercel.com/) in one click.', - demo: ( - - Deploy with Vercel - - ), - }, - { - title: turboIntegrations.disco.name, - description: turboIntegrations.disco.description, - href: turboIntegrations.disco.href, - demo: ( -
- Disco logo -
- ), - }, - { - title: 'Sign-In With Ethereum', - description: turboIntegrations.siwe.description, - href: turboIntegrations.siwe.href, - demo: ( -
- Prisma logo -
- ), - }, - { - title: 'Rainbowkit', - description: 'The best way to connect a wallet. Designed for everyone. Built for developers.', - demo: ( -
- Rainbow logo -
- ), - }, - { - title: turboIntegrations.etherscan.name, - description: turboIntegrations.etherscan.description, - href: turboIntegrations.etherscan.href, - demo: ( -
- - Etherscan logo - - - Etherscan logo - -
- ), - }, - { - title: 'Web3 Login', - description: 'Authenticate using an Ethereum Account', - demo: ( -
- - - - - - - - - - - -
- ), - }, - { - title: 'ERC20 WAGMI', - description: 'Read and Write to ERC20 smart contracts using minimal UI components.', - demo: ( -
- {`Token -

- ( - ) -

-

- Decimals -

- - View Token Page - -
- ), - }, - { - title: 'ERC721 WAGMI', - description: 'Read and Write to ERC721 smart contracts using minimal UI components.', - demo: ( -
- - - - View Token Page - -
- ), - }, - { - title: turboIntegrations.sessionKeys.name, - description: turboIntegrations.sessionKeys.description, - href: turboIntegrations.sessionKeys.href, - demo: ( -
- - Session keys logo - - - Session keys logo - -
- ), - }, - { - title: turboIntegrations.litProtocol.name, - description: turboIntegrations.litProtocol.description, - href: turboIntegrations.litProtocol.href, - demo: ( -
- - Lit Protocol logo - - - Lit Protocol logo - -
- ), - }, - { - title: turboIntegrations.openai.name, - description: turboIntegrations.openai.description, - href: turboIntegrations.openai.href, - demo: ( -
- - OpenAI logo - - - OpenAI logo - -
- ), - }, - { - title: turboIntegrations.pooltogether_v4.name, - description: turboIntegrations.pooltogether_v4.description, - href: turboIntegrations.pooltogether_v4.href, - demo: ( -
- - PoolTogether logo - - - PoolTogether logo - -
- ), - }, - { - title: turboIntegrations.livepeer.name, - description: turboIntegrations.livepeer.description, - href: turboIntegrations.livepeer.href, - demo: ( -
- - Livepeer logo - - - Livepeer logo - -
- ), - }, - { - title: turboIntegrations.connext.name, - description: turboIntegrations.connext.description, - href: turboIntegrations.connext.href, - demo: ( -
- - {`${turboIntegrations.connext.name} - - - {`${turboIntegrations.connext.name} - -
- ), - }, - { - title: turboIntegrations.gelato.name, - description: turboIntegrations.gelato.description, - href: turboIntegrations.gelato.href, - demo: ( -
- - {`${turboIntegrations.gelato.name} - - - {`${turboIntegrations.gelato.name} - -
- ), - }, - { - title: turboIntegrations.push_protocol.name, - description: turboIntegrations.push_protocol.description, - href: turboIntegrations.push_protocol.href, - demo: ( -
- - Push Protocol logo - - - Push Protocol logo - -
- ), - }, - { - title: turboIntegrations.moralis.name, - description: turboIntegrations.moralis.description, - href: turboIntegrations.moralis.href, - demo: ( -
- - Moralis logo - - - Mtarter logo - -
- ), - }, - { - title: turboIntegrations.aave.name, - description: turboIntegrations.aave.description, - href: turboIntegrations.aave.href, - demo: ( -
- - Aave logo - - - Aave logo - -
- ), - }, - { - title: turboIntegrations.arweave.name, - description: turboIntegrations.arweave.description, - href: turboIntegrations.arweave.href, - demo: ( -
- - Arweave logo - - - Arweave logo - -
- ), - }, - { - title: turboIntegrations.starter.name, - description: turboIntegrations.starter.description, - href: turboIntegrations.starter.href, - demo: ( -
- - Starter logo - - - Starter logo - -
- ), - }, -] diff --git a/template/base/app/admin/layout.tsx b/template/base/app/admin/layout.tsx deleted file mode 100644 index b25b53d..0000000 --- a/template/base/app/admin/layout.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { ReactNode } from 'react' - -import Image from 'next/image' - -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import { DashboardFooter } from '@/components/layout/dashboard-footer' -import { DashboardHeader } from '@/components/layout/dashboard-header' -import { MenuAdminSidebar } from '@/components/layout/menu-admin-sidebar' -import { UserDropdown } from '@/components/layout/user-dropdown' -import { IsDarkTheme } from '@/components/shared/is-dark-theme' -import { IsDesktop } from '@/components/shared/is-desktop' -import { IsLightTheme } from '@/components/shared/is-light-theme' -import { IsMobile } from '@/components/shared/is-mobile' -import { LinkComponent } from '@/components/shared/link-component' -import { siteConfig } from '@/config/site' - -export default function AdminLayout({ children }: { children: ReactNode }) { - return ( -
-
- -
- - - Logo - - - Logo - - -
- -
-
-
- -
- - - Logo - - - Logo - -

{siteConfig.name}

-
-
-
- -
-
- - - Dashboard - -
-
- -
-
-
- -
{children}
-
-
- ) -} diff --git a/template/base/app/admin/page.tsx b/template/base/app/admin/page.tsx deleted file mode 100644 index 404a975..0000000 --- a/template/base/app/admin/page.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client' - -import AppUsersTable from '@/components/app/app-users-table' -import { ButtonSIWELogin } from '@/integrations/siwe/components/button-siwe-login' -import { IsSignedIn } from '@/integrations/siwe/components/is-signed-in' -import { IsSignedOut } from '@/integrations/siwe/components/is-signed-out' -import { useGetAppUsers } from '@/lib/hooks/app/use-get-app-users' -import { useUser } from '@/lib/hooks/use-user' - -export default function PageDashboardTransactions() { - return ( -
-
-

Application Users

- -
- Authenticate to access admin area. - -
-
-
-
- - - -
- ) -} - -const RenderUserTable = () => { - const { user } = useUser() - const { isLoading, isError, data } = useGetAppUsers(user) - if (isError) return
Unauthorized Access
- return
{!isLoading && }
-} diff --git a/template/base/app/api/app/user/route.ts b/template/base/app/api/app/user/route.ts index 3a273cb..f36c9ab 100644 --- a/template/base/app/api/app/user/route.ts +++ b/template/base/app/api/app/user/route.ts @@ -1,6 +1,6 @@ -import { getIronSession } from 'iron-session' +import { getIronSession } from "iron-session" -import { SERVER_SESSION_SETTINGS } from '@/lib/session' +import { SERVER_SESSION_SETTINGS } from "@/lib/session" export async function GET(req: Request) { const res = new Response() @@ -13,14 +13,14 @@ export async function GET(req: Request) { isLoggedIn: true, isAdmin: session.isAdmin, }), - { status: 200, headers: { 'Content-Type': 'application/json' } } + { status: 200, headers: { "Content-Type": "application/json" } } ) } else { return new Response( JSON.stringify({ isLoggedIn: false, }), - { status: 200, headers: { 'Content-Type': 'application/json' } } + { status: 200, headers: { "Content-Type": "application/json" } } ) } } diff --git a/template/base/app/api/app/users/route.ts b/template/base/app/api/app/users/route.ts index 6d7fb55..2a675b8 100644 --- a/template/base/app/api/app/users/route.ts +++ b/template/base/app/api/app/users/route.ts @@ -1,8 +1,8 @@ -import { getIronSession } from 'iron-session' +import { env } from "@/env.mjs" +import { getIronSession } from "iron-session" -import { env } from '@/env.mjs' -import { prisma } from '@/lib/prisma' -import { SERVER_SESSION_SETTINGS } from '@/lib/session' +import { prisma } from "@/lib/prisma" +import { SERVER_SESSION_SETTINGS } from "@/lib/session" export type Users = Awaited> @@ -12,14 +12,14 @@ export async function GET(req: Request) { const session = await getIronSession(req, res, SERVER_SESSION_SETTINGS) const isAdmin = session.isAdmin if (!isAdmin) { - return new Response('Unauthorized', { status: 401 }) + return new Response("Unauthorized", { status: 401 }) } let users: Users = [] if (env.DATABASE_URL) { users = await prisma.user.findMany() } - return new Response(JSON.stringify({ users, object: 'Users' })) + return new Response(JSON.stringify({ users, object: "Users" })) } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) return new Response(errorMessage, { status: 500 }) diff --git a/template/base/app/api/siwe/logout/route.ts b/template/base/app/api/siwe/logout/route.ts index e5a37c2..dcc4bff 100644 --- a/template/base/app/api/siwe/logout/route.ts +++ b/template/base/app/api/siwe/logout/route.ts @@ -1 +1 @@ -export { GET } from '@/integrations/siwe/api/logout' +export { GET } from "@/integrations/siwe/api/logout" diff --git a/template/base/app/api/siwe/nonce/route.ts b/template/base/app/api/siwe/nonce/route.ts index 926d0a0..362be83 100644 --- a/template/base/app/api/siwe/nonce/route.ts +++ b/template/base/app/api/siwe/nonce/route.ts @@ -1 +1 @@ -export { GET } from '@/integrations/siwe/api/nonce' +export { GET } from "@/integrations/siwe/api/nonce" diff --git a/template/base/app/api/siwe/route.ts b/template/base/app/api/siwe/route.ts index 629fb70..8305ac0 100644 --- a/template/base/app/api/siwe/route.ts +++ b/template/base/app/api/siwe/route.ts @@ -1 +1 @@ -export { GET } from '@/integrations/siwe/api' +export { GET } from "@/integrations/siwe/api" diff --git a/template/base/app/api/siwe/verify/route.ts b/template/base/app/api/siwe/verify/route.ts index f89c56f..51d757a 100644 --- a/template/base/app/api/siwe/verify/route.ts +++ b/template/base/app/api/siwe/verify/route.ts @@ -1 +1 @@ -export { POST } from '@/integrations/siwe/api/verify' +export { POST } from "@/integrations/siwe/api/verify" diff --git a/template/base/app/dashboard/account/page.tsx b/template/base/app/dashboard/account/page.tsx index 33b4510..7cd4bf3 100644 --- a/template/base/app/dashboard/account/page.tsx +++ b/template/base/app/dashboard/account/page.tsx @@ -1,28 +1,32 @@ -'use client' -import { motion } from 'framer-motion' +"use client" -import { WalletAddress } from '@/components/blockchain/wallet-address' -import { WalletBalance } from '@/components/blockchain/wallet-balance' -import { WalletNonce } from '@/components/blockchain/wallet-nonce' -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' +import { motion } from "framer-motion" + +import { FADE_DOWN_ANIMATION_VARIANTS } from "@/config/design" +import { Card } from "@/components/ui/card" +import { WalletAddress } from "@/components/blockchain/wallet-address" +import { WalletBalance } from "@/components/blockchain/wallet-balance" +import { WalletNonce } from "@/components/blockchain/wallet-nonce" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" export default function PageDashboardAccount() { return ( + whileInView="show" + > -
+

Account


- Address: + Address:{" "} +
Balance: @@ -31,10 +35,12 @@ export default function PageDashboardAccount() { Nonce:

-
+
-

Connect Wallet to view your personalized dashboard.

+

+ Connect Wallet to view your personalized dashboard. +

) diff --git a/template/base/app/dashboard/admin/page.tsx b/template/base/app/dashboard/admin/page.tsx new file mode 100644 index 0000000..a13e008 --- /dev/null +++ b/template/base/app/dashboard/admin/page.tsx @@ -0,0 +1,44 @@ +"use client" + +import { useGetAppUsers } from "@/lib/hooks/app/use-get-app-users" +import { useUser } from "@/lib/hooks/use-user" +import AppUsersTable from "@/components/app/app-users-table" +import { ButtonSIWELogin } from "@/integrations/siwe/components/button-siwe-login" +import { IsSignedIn } from "@/integrations/siwe/components/is-signed-in" +import { IsSignedOut } from "@/integrations/siwe/components/is-signed-out" + +export default function PageDashboardTransactions() { + return ( +
+
+

Application Users

+ +
+ + Authenticate to access admin area. + + +
+
+
+
+ + + +
+ ) +} + +const RenderUserTable = () => { + const { user } = useUser() + const { isLoading, isError, data } = useGetAppUsers(user) + if (isError) + return
Unauthorized Access
+ return ( +
+ {!isLoading && ( + + )} +
+ ) +} diff --git a/template/base/app/dashboard/layout.tsx b/template/base/app/dashboard/layout.tsx index c6b9423..d96626f 100644 --- a/template/base/app/dashboard/layout.tsx +++ b/template/base/app/dashboard/layout.tsx @@ -1,71 +1,59 @@ -import { ReactNode } from 'react' +import Link from "next/link" +import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa" -import Image from 'next/image' +import { menuAdmin } from "@/config/menu-admin" +import { menuDashboard } from "@/config/menu-dashboard" +import { siteConfig } from "@/config/site" +import { ScrollArea } from "@/components/ui/scroll-area" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { SidebarNav } from "@/components/layout/sidebar-nav" +import { SiteHeader } from "@/components/layout/site-header" -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import { DashboardFooter } from '@/components/layout/dashboard-footer' -import { DashboardHeader } from '@/components/layout/dashboard-header' -import { MenuDashboardSidebar } from '@/components/layout/menu-dashboard-sidebar' -import { UserDropdown } from '@/components/layout/user-dropdown' -import { IsDarkTheme } from '@/components/shared/is-dark-theme' -import { IsDesktop } from '@/components/shared/is-desktop' -import { IsLightTheme } from '@/components/shared/is-light-theme' -import { IsMobile } from '@/components/shared/is-mobile' -import { LinkComponent } from '@/components/shared/link-component' -import { Toaster } from '@/components/ui/toaster' -import { siteConfig } from '@/config/site' +interface DashboardLayoutProps { + children: React.ReactNode +} -export default function DashboardLayout({ children }: { children: ReactNode }) { +export default function DashboardLayout({ children }: DashboardLayoutProps) { return ( - <> -
-
- -
- - - Logo - - - Logo - - -
- -
-
-
- -
- - - Logo - - - Logo - -

{siteConfig.name}

-
-
-
- +
+ +
+
-
- -
{children}
-
+ + +
{children}
+
+
+
- - +
) } diff --git a/template/base/app/dashboard/page.tsx b/template/base/app/dashboard/page.tsx index 27d1ec0..ad38c5d 100644 --- a/template/base/app/dashboard/page.tsx +++ b/template/base/app/dashboard/page.tsx @@ -1,36 +1,37 @@ -'use client' +"use client" -import { motion } from 'framer-motion' +import { motion } from "framer-motion" -import { WalletAddress } from '@/components/blockchain/wallet-address' -import { WalletBalance } from '@/components/blockchain/wallet-balance' -import { WalletEnsName } from '@/components/blockchain/wallet-ens-name' -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' +import { FADE_DOWN_ANIMATION_VARIANTS } from "@/config/design" +import { WalletAddress } from "@/components/blockchain/wallet-address" +import { WalletBalance } from "@/components/blockchain/wallet-balance" +import { WalletEnsName } from "@/components/blockchain/wallet-ens-name" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" export default function PageDashboard() { return ( + whileInView="show" + > -
+
-

- +

+ hi 👋

- - Balance: ETH + + Balance: ETH
@@ -38,7 +39,9 @@ export default function PageDashboard() {

-

Connect Wallet to view your personalized dashboard.

+

+ Connect Wallet to view your personalized dashboard. +

) diff --git a/template/base/app/dashboard/transactions/page.tsx b/template/base/app/dashboard/transactions/page.tsx index 1017ca3..b8bd814 100644 --- a/template/base/app/dashboard/transactions/page.tsx +++ b/template/base/app/dashboard/transactions/page.tsx @@ -1,31 +1,33 @@ -'use client' +"use client" -import { useNetwork } from 'wagmi' +import { useNetwork } from "wagmi" -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { TransactionsTable } from '@/integrations/etherscan/components/transactions-table' -import { useEtherscanAccountTransactions } from '@/integrations/etherscan/hooks/use-etherscan-account-transactions' -import { ButtonSIWELogin } from '@/integrations/siwe/components/button-siwe-login' -import { IsSignedIn } from '@/integrations/siwe/components/is-signed-in' -import { IsSignedOut } from '@/integrations/siwe/components/is-signed-out' -import { useUser } from '@/lib/hooks/use-user' +import { useUser } from "@/lib/hooks/use-user" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { TransactionsTable } from "@/integrations/etherscan/components/transactions-table" +import { useEtherscanAccountTransactions } from "@/integrations/etherscan/hooks/use-etherscan-account-transactions" +import { ButtonSIWELogin } from "@/integrations/siwe/components/button-siwe-login" +import { IsSignedIn } from "@/integrations/siwe/components/is-signed-in" +import { IsSignedOut } from "@/integrations/siwe/components/is-signed-out" export default function PageDashboardTransactions() { return ( -
+

Transactions

- Login to access the TurboETH free API - + + Login to access the TurboETH free API + +
- Connect wallet and login to access page + Connect wallet and login to access page

@@ -46,5 +48,11 @@ const Table = () => { [user] ) - return
{!isLoading && }
+ return ( +
+ {!isLoading && ( + + )} +
+ ) } diff --git a/template/base/app/layout.tsx b/template/base/app/layout.tsx index 7e1509a..04d7826 100644 --- a/template/base/app/layout.tsx +++ b/template/base/app/layout.tsx @@ -1,62 +1,52 @@ -import '@/styles/app.css' -import '@/styles/gradient.css' -import '@/styles/periphery.css' -import { ReactNode } from 'react' +import "@/styles/app.css" +import "@/styles/globals.css" -import { Raleway } from 'next/font/google' -import { Inter as FontSans } from 'next/font/google' -import localFont from 'next/font/local' +import { ReactNode } from "react" +import { env } from "@/env.mjs" -import RootProvider from '@/components/providers/root-provider' -import { siteConfig } from '@/config/site' -import { env } from '@/env.mjs' -import { cn } from '@/lib/utils' +import { siteConfig } from "@/config/site" +import { fontSans } from "@/lib/fonts" +import { cn } from "@/lib/utils" +import { Toaster } from "@/components/ui/toaster" +import RootProvider from "@/components/providers/root-provider" -const url = env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000' +const url = env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000" export const metadata = { metadataBase: new URL(url), title: `${siteConfig.name} - ${siteConfig.description}`, description: siteConfig.description, + manifest: "/manifest.json", icons: { - icon: '/favicon.ico', + icon: "/favicon.ico", }, + themeColor: "#feefc4", openGraph: { title: siteConfig.name, description: siteConfig.description, url: url?.toString(), siteName: siteConfig.name, - type: 'website', + type: "website", }, twitter: { - card: 'summary_large_image', + card: "summary_large_image", title: siteConfig.name, description: siteConfig.description, }, } -const sfPro = localFont({ - src: '../assets/fonts/SF-Pro-Display-Medium.otf', - variable: '--sfPro-font', -}) - -const raleway = Raleway({ - subsets: ['latin'], - weight: ['100', '200', '400', '500', '600', '700', '800', '900'], - variable: '--raleway-font', -}) - -const fontSans = FontSans({ - subsets: ['latin'], - variable: '--font-sans', -}) - export default function RootLayout({ children }: { children: ReactNode }) { return ( <> - - + + {children} + diff --git a/template/base/app/opengraph-image.png b/template/base/app/opengraph-image.png new file mode 100644 index 0000000000000000000000000000000000000000..62dda5568ae211db73568cd2521a2e46da7a0e46 GIT binary patch literal 232895 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
@@ -57,8 +70,13 @@ export default async function Image() { { fonts: [ { - name: 'SF Pro', - data: await fetch(new URL('../assets/fonts/SF-Pro-Display-Medium.otf', import.meta.url)).then((res) => res.arrayBuffer()), + name: "SF Pro", + data: await fetch( + new URL( + "../assets/fonts/SF-Pro-Display-Medium.otf", + import.meta.url + ) + ).then((res) => res.arrayBuffer()), }, ], } diff --git a/template/base/app/twitter-image.tsx b/template/base/app/twitter-image.tsx deleted file mode 100644 index 2583aba..0000000 --- a/template/base/app/twitter-image.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import Image from './opengraph-image' - -export const runtime = 'edge' - -export const alt = 'TurboETH Logo' -export const size = { - width: 1200, - height: 630, -} - -export const contentType = 'image/png' - -export default Image diff --git a/template/base/commitlint.config.js b/template/base/commitlint.config.js index 4fedde6..858aaa8 100644 --- a/template/base/commitlint.config.js +++ b/template/base/commitlint.config.js @@ -1 +1 @@ -module.exports = { extends: ['@commitlint/config-conventional'] } +module.exports = { extends: ["@commitlint/config-conventional"] } diff --git a/template/base/components/app/app-users-table.tsx b/template/base/components/app/app-users-table.tsx index 1cf66bb..23a0feb 100644 --- a/template/base/components/app/app-users-table.tsx +++ b/template/base/components/app/app-users-table.tsx @@ -1,13 +1,13 @@ -import { HTMLAttributes, useMemo } from 'react' +import { HTMLAttributes, useMemo } from "react" +import { Address } from "wagmi" -import { Address } from 'wagmi' +import { Address as AddressComponent } from "@/components/blockchain/address" +import type { Users } from "@/app/api/app/users/route" -import type { Users } from '@/app/api/app/users/route' -import { Address as AddressComponent } from '@/components/blockchain/address' - -import TableCore from '../shared/table/table-core' -import { TimeFromUtc } from '../shared/time-from-utc' -import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover' +import TableCore from "../shared/table/table-core" +import { TimeFromUtc } from "../shared/time-from-utc" +import { Badge } from "../ui/badge" +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover" interface AppUsersTableProps extends HTMLAttributes { data: Users | undefined @@ -17,24 +17,32 @@ function AppUsersTable({ data, className, ...props }: AppUsersTableProps) { const columns = useMemo( () => [ { - Header: 'Address', - accessor: 'address', - Cell: ({ value }: { value: Address }) => , + Header: "Address", + accessor: "address", + Cell: ({ value }: { value: Address }) => ( + + ), }, { - Header: 'Created', - accessor: 'createdAt', - Cell: ({ value }: { value: string }) => , + Header: "Created", + accessor: "createdAt", + Cell: ({ value }: { value: string }) => ( + + ), }, { Header: () => null, - id: 'actions', - accessor: 'id', + id: "actions", + accessor: "id", Cell: () => (
- Profile + Profile Add user profile information 🥳 @@ -45,7 +53,9 @@ function AppUsersTable({ data, className, ...props }: AppUsersTableProps) { [] ) if (!data) return null - return + return ( + + ) } export default AppUsersTable diff --git a/template/base/components/blockchain/address.tsx b/template/base/components/blockchain/address.tsx index f454588..b3b2a0f 100644 --- a/template/base/components/blockchain/address.tsx +++ b/template/base/components/blockchain/address.tsx @@ -1,23 +1,35 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { useNetwork, type Address as AddressType } from "wagmi" -import { type Address as AddressType, useNetwork } from 'wagmi' +import { LinkComponent } from "../shared/link-component" -import { LinkComponent } from '../shared/link-component' - -interface AddressProps extends Omit, 'children'> { +interface AddressProps extends Omit, "children"> { address: AddressType truncate?: boolean isLink?: boolean } -export const Address = ({ address, className, truncate, isLink, ...props }: AddressProps) => { +export const Address = ({ + address, + className, + truncate, + isLink, + ...props +}: AddressProps) => { const { chain } = useNetwork() const blockExplorerUrl = chain?.blockExplorers?.default.url - const formattedAddress = truncate ? `${address.slice(0, 6)}...${address.slice(-4)}` : address + const formattedAddress = truncate + ? `${address.slice(0, 6)}...${address.slice(-4)}` + : address if (isLink && blockExplorerUrl) { return ( - + {formattedAddress} ) diff --git a/template/base/components/blockchain/block-explorer-link.tsx b/template/base/components/blockchain/block-explorer-link.tsx index 26b6933..456f89d 100644 --- a/template/base/components/blockchain/block-explorer-link.tsx +++ b/template/base/components/blockchain/block-explorer-link.tsx @@ -1,25 +1,38 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { Address, useNetwork } from "wagmi" -import { Address, useNetwork } from 'wagmi' - -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" interface BlockExplorerLinkProps extends HTMLAttributes { address: Address | undefined showExplorerName?: boolean - type?: 'address' | 'tx' + type?: "address" | "tx" } -export const BlockExplorerLink = ({ address, children, className, showExplorerName, type = 'address', ...props }: BlockExplorerLinkProps) => { +export const BlockExplorerLink = ({ + address, + children, + className, + showExplorerName, + type = "address", + ...props +}: BlockExplorerLinkProps) => { const { chain } = useNetwork() const blockExplorer = chain?.blockExplorers?.default if (!address) return null return ( - + {blockExplorer && ( - + {showExplorerName ? blockExplorer.name : children ?? address} )} diff --git a/template/base/components/blockchain/contract-write-button.tsx b/template/base/components/blockchain/contract-write-button.tsx index 9b39158..870a781 100644 --- a/template/base/components/blockchain/contract-write-button.tsx +++ b/template/base/components/blockchain/contract-write-button.tsx @@ -1,8 +1,11 @@ -import { ButtonHTMLAttributes } from 'react' +import { ButtonHTMLAttributes } from "react" -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" -interface ContractWriteButtonProps extends ButtonHTMLAttributes { +import { Button } from "../ui/button" + +interface ContractWriteButtonProps + extends ButtonHTMLAttributes { isLoadingTx: boolean isLoadingWrite: boolean write?: boolean @@ -17,13 +20,21 @@ export const ContractWriteButton = ({ isLoadingTx, isLoadingWrite, write = true, - loadingWriteText = 'Sign the transaction in your wallet', - loadingTxText = 'Writing...', + loadingWriteText = "Sign the transaction in your wallet", + loadingTxText = "Writing...", ...props }: ContractWriteButtonProps) => { return ( - + ) } diff --git a/template/base/components/blockchain/handle-wallet-events.tsx b/template/base/components/blockchain/handle-wallet-events.tsx index df55fa9..bb8efc4 100644 --- a/template/base/components/blockchain/handle-wallet-events.tsx +++ b/template/base/components/blockchain/handle-wallet-events.tsx @@ -1,10 +1,10 @@ -'use client' -import { ReactNode } from 'react' +"use client" -import { useAccount } from 'wagmi' +import { ReactNode } from "react" +import { useAccount } from "wagmi" -import { siweLogout } from '@/integrations/siwe/actions/siwe-logout' -import { useUser } from '@/lib/hooks/use-user' +import { useUser } from "@/lib/hooks/use-user" +import { siweLogout } from "@/integrations/siwe/actions/siwe-logout" interface HandleWalletEventsProps { children: ReactNode diff --git a/template/base/components/blockchain/network-status.tsx b/template/base/components/blockchain/network-status.tsx index e457d48..be1b7e0 100644 --- a/template/base/components/blockchain/network-status.tsx +++ b/template/base/components/blockchain/network-status.tsx @@ -1,36 +1,42 @@ -'use client' +"use client" -import { useBlockNumber, useNetwork } from 'wagmi' +import Link from "next/link" +import { useBlockNumber, useNetwork } from "wagmi" -import { cn } from '@/lib/utils' -import { GetNetworkColor } from '@/lib/utils/get-network-color' +import { cn } from "@/lib/utils" +import { GetNetworkColor } from "@/lib/utils/get-network-color" +import { Badge } from "@/components/ui/badge" -import { LinkComponent } from '../shared/link-component' +const badgeVariants: Record, string> = { + green: "bg-green-200 text-green-700", + blue: "bg-blue-200 text-blue-700", + red: "bg-red-200 text-red-700", + purple: "bg-purple-200 text-purple-700", + gray: "bg-gray-200 text-gray-700", + yellow: "bg-yellow-200 text-yellow-700", +} -type NetworkStatusProps = React.HTMLAttributes +export function NetworkStatus() { + const { data } = useBlockNumber() + const { chain } = useNetwork() + const blockExplorerUrl = chain?.blockExplorers?.default.url -export function NetworkStatus({ className, ...props }: NetworkStatusProps) { - const block = useBlockNumber({ watch: true }) - const network = useNetwork() - const explorerUrl = network.chain?.blockExplorers?.default.url - const classes = cn(className, 'dark:bg-gray-800 bg-gray-100 z-10 flex shadow-md items-center rounded-full overflow-hidden') - const classesBadge = cn( - 'uppercase text-xs font-bold tracking-wider leading-none rounded-full px-2', - `bg-${GetNetworkColor(network.chain?.network)}-200`, - `text-${GetNetworkColor(network.chain?.network)}-700 dark:text-${GetNetworkColor(network.chain?.network)}-700 py-2` - ) + if (!chain || !blockExplorerUrl) return null return ( -
- - {network.chain?.name ?? 'Ethereum'} - - {explorerUrl && ( - - <>#{block.data?.toString()} - - )} - {!explorerUrl && # {block.data?.toString()}} -
+ + + {chain.name} + +

#{data?.toString()}

+ ) } diff --git a/template/base/components/blockchain/transaction-status.tsx b/template/base/components/blockchain/transaction-status.tsx index 01a5d4c..65a4dd6 100644 --- a/template/base/components/blockchain/transaction-status.tsx +++ b/template/base/components/blockchain/transaction-status.tsx @@ -1,10 +1,9 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { BaseError } from "viem" -import { BaseError } from 'viem' +import { cn } from "@/lib/utils" -import { cn } from '@/lib/utils' - -import { BlockExplorerLink } from './block-explorer-link' +import { BlockExplorerLink } from "./block-explorer-link" interface TransactionStatusProps extends HTMLAttributes { error?: BaseError @@ -14,18 +13,33 @@ interface TransactionStatusProps extends HTMLAttributes { hash?: `0x${string}` } -export const TransactionStatus = ({ className, error, isError, isLoadingTx, isSuccess, hash, ...props }: TransactionStatusProps) => { +export const TransactionStatus = ({ + className, + error, + isError, + isLoadingTx, + isSuccess, + hash, + ...props +}: TransactionStatusProps) => { return ( <> -
+
{(isLoadingTx || isSuccess) && ( <> - {isLoadingTx ? 'Processing transaction...' : 'Success!'} + {isLoadingTx ? "Processing transaction..." : "Success!"} )}
- {isError &&
Error: {error?.shortMessage}
} + {isError && ( +
+ Error: {error?.shortMessage} +
+ )} ) } diff --git a/template/base/components/blockchain/wallet-address.tsx b/template/base/components/blockchain/wallet-address.tsx index 6accaff..e7cf0ec 100644 --- a/template/base/components/blockchain/wallet-address.tsx +++ b/template/base/components/blockchain/wallet-address.tsx @@ -1,17 +1,30 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { useAccount } from "wagmi" -import { useAccount } from 'wagmi' +import { Address } from "./address" -import { Address } from './address' - -export interface WalletAddressProps extends Omit, 'children'> { +export interface WalletAddressProps + extends Omit, "children"> { truncate?: boolean isLink?: boolean } -export const WalletAddress = ({ className, truncate, isLink, ...props }: WalletAddressProps) => { +export const WalletAddress = ({ + className, + truncate, + isLink, + ...props +}: WalletAddressProps) => { const { address } = useAccount() if (!address) return null - return
+ return ( +
+ ) } diff --git a/template/base/components/blockchain/wallet-balance.tsx b/template/base/components/blockchain/wallet-balance.tsx index eac0bf3..119e3f3 100644 --- a/template/base/components/blockchain/wallet-balance.tsx +++ b/template/base/components/blockchain/wallet-balance.tsx @@ -1,14 +1,18 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { useAccount, useBalance } from "wagmi" -import { useAccount, useBalance } from 'wagmi' +import { trimFormattedBalance } from "@/lib/utils" -import { trimFormattedBalance } from '@/lib/utils' - -interface WalletBalanceProps extends Omit, 'children'> { +interface WalletBalanceProps + extends Omit, "children"> { decimals?: number } -export const WalletBalance = ({ className, decimals = 4, ...props }: WalletBalanceProps) => { +export const WalletBalance = ({ + className, + decimals = 4, + ...props +}: WalletBalanceProps) => { const { address } = useAccount() const { data: balance } = useBalance({ address, diff --git a/template/base/components/blockchain/wallet-connect-custom.tsx b/template/base/components/blockchain/wallet-connect-custom.tsx index 2bcf7fe..82688dc 100644 --- a/template/base/components/blockchain/wallet-connect-custom.tsx +++ b/template/base/components/blockchain/wallet-connect-custom.tsx @@ -1,6 +1,7 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { ConnectButton } from "@rainbow-me/rainbowkit" -import { ConnectButton } from '@rainbow-me/rainbowkit' +import { Button } from "../ui/button" interface WalletConnectCustomProps extends HTMLAttributes { classNameConnect?: string @@ -12,17 +13,23 @@ interface WalletConnectCustomProps extends HTMLAttributes { export const WalletConnectCustom = ({ className, - classNameConnect = 'btn btn-primary w-full', - classNameConnected = 'btn btn-primary w-full', - classNameWrongNetwork = 'btn btn-red w-full', - labelConnect = 'Connect Wallet', - labelWrongNetwork = 'Wrong Network', + labelConnect = "Connect Wallet", + labelWrongNetwork = "Wrong Network", ...props }: WalletConnectCustomProps) => { return ( - {({ account, chain, openChainModal, openConnectModal, authenticationStatus }) => { - const connected = account && chain && (!authenticationStatus || authenticationStatus === 'authenticated') + {({ + account, + chain, + openChainModal, + openConnectModal, + authenticationStatus, + }) => { + const connected = + account && + chain && + (!authenticationStatus || authenticationStatus === "authenticated") return (
@@ -30,24 +37,24 @@ export const WalletConnectCustom = ({ if (!connected) { return ( <> - + ) } if (chain.unsupported) { return ( - + ) } return ( -
- +
) })()} diff --git a/template/base/components/blockchain/wallet-connect.tsx b/template/base/components/blockchain/wallet-connect.tsx index 3b9a945..6062274 100644 --- a/template/base/components/blockchain/wallet-connect.tsx +++ b/template/base/components/blockchain/wallet-connect.tsx @@ -1,19 +1,21 @@ -import { HtmlHTMLAttributes } from 'react' +import { HtmlHTMLAttributes } from "react" +import { ConnectButton } from "@rainbow-me/rainbowkit" -import { ConnectButton } from '@rainbow-me/rainbowkit' - -export const WalletConnect = ({ className, ...props }: HtmlHTMLAttributes) => { +export const WalletConnect = ({ + className, + ...props +}: HtmlHTMLAttributes) => { return ( diff --git a/template/base/components/blockchain/wallet-ens-name.tsx b/template/base/components/blockchain/wallet-ens-name.tsx index b4ce491..a95d4d9 100644 --- a/template/base/components/blockchain/wallet-ens-name.tsx +++ b/template/base/components/blockchain/wallet-ens-name.tsx @@ -1,8 +1,10 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { useAccount, useEnsName } from "wagmi" -import { useAccount, useEnsName } from 'wagmi' - -export const WalletEnsName = ({ className, ...props }: Omit, 'children'>) => { +export const WalletEnsName = ({ + className, + ...props +}: Omit, "children">) => { const { address } = useAccount() // Chain ID is hardcoded to 1 since wagmi ENS resolve is only available on Ethereum Mainnet const { data: ensName, isSuccess } = useEnsName({ address, chainId: 1 }) diff --git a/template/base/components/blockchain/wallet-nonce.tsx b/template/base/components/blockchain/wallet-nonce.tsx index 476e31a..5aa5cc9 100644 --- a/template/base/components/blockchain/wallet-nonce.tsx +++ b/template/base/components/blockchain/wallet-nonce.tsx @@ -1,14 +1,13 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { useAccount, usePublicClient, useQuery } from "wagmi" -import { useAccount, usePublicClient, useQuery } from 'wagmi' - -type WalletNonceProps = Omit, 'children'> +type WalletNonceProps = Omit, "children"> export const WalletNonce = ({ className, ...props }: WalletNonceProps) => { const publicClient = usePublicClient() const { address } = useAccount() - const { data: nonce } = useQuery(['wallet-nonce', address, publicClient], { + const { data: nonce } = useQuery(["wallet-nonce", address, publicClient], { queryFn: async () => { if (!publicClient || !address) return return await publicClient.getTransactionCount({ diff --git a/template/base/components/layout/dashboard-footer.tsx b/template/base/components/layout/dashboard-footer.tsx deleted file mode 100644 index 4af77e0..0000000 --- a/template/base/components/layout/dashboard-footer.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { HTMLAttributes } from 'react' - -import { FaGithub, FaTwitter } from 'react-icons/fa' - -import { siteConfig } from '@/config/site' -import { cn } from '@/lib/utils' - -import { LinkComponent } from '../shared/link-component' - -export function DashboardFooter({ className, ...props }: HTMLAttributes) { - const classes = cn(className, 'flex flex-col justify-center') - - return ( - - ) -} diff --git a/template/base/components/layout/dashboard-header.tsx b/template/base/components/layout/dashboard-header.tsx deleted file mode 100644 index 638fd5d..0000000 --- a/template/base/components/layout/dashboard-header.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import { HTMLAttributes } from 'react' - -import { CopyToClipboard } from 'react-copy-to-clipboard' -import { FaCopy } from 'react-icons/fa' -import { useAccount } from 'wagmi' - -import { WalletAddress } from '@/components/blockchain/wallet-address' -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import { ButtonSIWELogin } from '@/integrations/siwe/components/button-siwe-login' -import { ButtonSIWELogout } from '@/integrations/siwe/components/button-siwe-logout' -import { IsSignedIn } from '@/integrations/siwe/components/is-signed-in' -import { IsSignedOut } from '@/integrations/siwe/components/is-signed-out' -import { useToast } from '@/lib/hooks/use-toast' -import { cn } from '@/lib/utils' - -import { IsWalletConnected } from '../shared/is-wallet-connected' -import { IsWalletDisconnected } from '../shared/is-wallet-disconnected' -import { ThemeToggle } from '../shared/theme-toggle' - -export function DashboardHeader({ className, ...props }: HTMLAttributes) { - const classes = cn(className, 'px-6 lg:px-10 py-3 flex items-center w-full') - const { address } = useAccount() - const { toast, dismiss } = useToast() - - const handleToast = () => { - toast({ - title: 'Addess Copied', - description: 'Your address has been copied to your clipboard.', - }) - - setTimeout(() => { - dismiss() - }, 4200) - } - - return ( -
-
- - - - - - - - - - - - - - -
- -
- - - - - - - - - - - - -
-
- ) -} diff --git a/template/base/components/layout/footer.tsx b/template/base/components/layout/footer.tsx index 86669d7..bcb12cd 100644 --- a/template/base/components/layout/footer.tsx +++ b/template/base/components/layout/footer.tsx @@ -1,29 +1,40 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import Link from "next/link" +import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa" -import { FaGithub, FaTwitter } from 'react-icons/fa' +import { siteConfig } from "@/config/site" +import { cn } from "@/lib/utils" -import { siteConfig } from '@/config/site' -import { cn } from '@/lib/utils' - -import { LinkComponent } from '../shared/link-component' +import { LinkComponent } from "../shared/link-component" +import { buttonVariants } from "../ui/button" export function Footer({ className, ...props }: HTMLAttributes) { - const classes = cn(className, 'px-4 py-6 flex flex-col justify-center items-center') + const classes = cn( + className, + "flex flex-col items-center justify-center px-4 py-6" + ) return ( ) diff --git a/template/base/components/layout/header.tsx b/template/base/components/layout/header.tsx deleted file mode 100644 index 61a9eae..0000000 --- a/template/base/components/layout/header.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client' - -import { HTMLAttributes } from 'react' - -import Image from 'next/image' - -import { siteConfig } from '@/config/site' -import useScroll from '@/lib/hooks/use-scroll' -import { cn } from '@/lib/utils' - -import { NavigationMenuGeneral } from './navigation-menu-general' -import { UserDropdown } from './user-dropdown' -import BranchButtonLoginOrAccount from '../../integrations/siwe/components/branch-button-login-or-account' -import { IsDarkTheme } from '../shared/is-dark-theme' -import { IsDesktop } from '../shared/is-desktop' -import { IsLightTheme } from '../shared/is-light-theme' -import { IsMobile } from '../shared/is-mobile' -import { LinkComponent } from '../shared/link-component' -import { ThemeToggle } from '../shared/theme-toggle' - -export function Header({ className, ...props }: HTMLAttributes) { - const scrolled = useScroll(50) - const classes = cn( - className, - 'fixed top-0 w-full', - 'px-6 lg:px-10 py-3 mb-8 flex items-center', - { - 'border-b border-gray-200 bg-white/50 backdrop-blur-xl dark:bg-black/50 dark:border-gray-800': scrolled, - }, - 'z-30 transition-all' - ) - return ( -
- -
- - - Logo - - - Logo - - -
- -
-
-
- - - - Logo - - - Logo - -

{siteConfig.name}

-
-
- -
-
- - - Dashboard - - -
-
-
- ) -} diff --git a/template/base/components/layout/main-nav.tsx b/template/base/components/layout/main-nav.tsx new file mode 100644 index 0000000..ed2a2d7 --- /dev/null +++ b/template/base/components/layout/main-nav.tsx @@ -0,0 +1,141 @@ +"use client" + +import React from "react" +import Link from "next/link" +import { + integrationCategories, + turboIntegrations, +} from "@/data/turbo-integrations" + +import { siteConfig } from "@/config/site" +import { + NavigationMenu, + NavigationMenuContent, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, +} from "@/components/ui/navigation-menu" +import { Separator } from "@/components/ui/separator" +import { LightDarkImage } from "@/components/shared/light-dark-image" + +import { LinkComponent } from "../shared/link-component" + +export function MainNav() { + return ( +
+ + + + {siteConfig.name} + + + +
+ ) +} + +function MainNavMenu() { + return ( + + + + Integrations + +
    + {integrationCategories.map((category) => { + const categoryIntegrations = Object.values( + turboIntegrations + ).filter((integration) => integration.category === category) + return ( + categoryIntegrations.length > 0 && ( + <> +

    + {category.charAt(0).toUpperCase() + category.slice(1)} +

    + + {categoryIntegrations.map( + ({ name, href, description, imgDark, imgLight }) => ( + + ) + )} + + ) + ) + })} +
+
+
+ + + + Documentation + + + +
+
+ ) +} + +interface NavMenuListItemProps { + name: string + description: string + href: string + lightImage: string + darkImage: string +} + +const NavMenuListItem = ({ + name, + description, + href, + lightImage, + darkImage, +}: NavMenuListItemProps) => { + return ( +
  • + + +
    + + {name} +
    +

    + {description} +

    +
    +
    +
  • + ) +} diff --git a/template/base/components/layout/menu-admin-sidebar.tsx b/template/base/components/layout/menu-admin-sidebar.tsx deleted file mode 100644 index 9a651d4..0000000 --- a/template/base/components/layout/menu-admin-sidebar.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client' -import { HTMLAttributes } from 'react' - -import { usePathname } from 'next/navigation' - -import { menuAdmin } from '@/config/menu-admin' -import { cn } from '@/lib/utils' - -import { LinkComponent } from '../shared/link-component' - -export const MenuAdminSidebar = ({ className, ...props }: HTMLAttributes) => { - const cx = cn(className, 'flex flex-col gap-1') - - const pathname = usePathname() - return ( -
    - {menuAdmin.map((item) => { - return ( - - {item.label} - - ) - })} -
    - ) -} - -interface ItemProps extends HTMLAttributes { - href: string - currentPath: string | null -} - -const Item = ({ children, href, currentPath }: ItemProps) => { - const cx = cn('menu-item my-2', { - active: currentPath === href, - }) - - return ( - - {children} - - ) -} diff --git a/template/base/components/layout/menu-dashboard-sidebar.tsx b/template/base/components/layout/menu-dashboard-sidebar.tsx deleted file mode 100644 index 375462d..0000000 --- a/template/base/components/layout/menu-dashboard-sidebar.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { HTMLAttributes } from 'react' - -import { usePathname } from 'next/navigation' - -import { menuDashboard } from '@/config/menu-dashboard' -import { cn } from '@/lib/utils' - -import { LinkComponent } from '../shared/link-component' - -export const MenuDashboardSidebar = ({ className }: HTMLAttributes) => { - const cx = cn(className, 'flex flex-col gap-1') - - const pathname = usePathname() - return ( -
    - {menuDashboard.map((item) => { - return ( - - {item.label} - - ) - })} -
    - ) -} - -interface ItemProps extends HTMLAttributes { - href: string - currentPath: string | null -} - -const Item = ({ children, href, currentPath, ...props }: ItemProps) => { - const cx = cn('menu-item my-2', { - active: currentPath === href, - }) - - return ( - - {children} - - ) -} diff --git a/template/base/components/layout/mobile-nav.tsx b/template/base/components/layout/mobile-nav.tsx new file mode 100644 index 0000000..20e457c --- /dev/null +++ b/template/base/components/layout/mobile-nav.tsx @@ -0,0 +1,224 @@ +"use client" + +import { useState } from "react" +import Link, { LinkProps } from "next/link" +import { useRouter } from "next/navigation" +import { + integrationCategories, + turboIntegrations, +} from "@/data/turbo-integrations" +import { LuMenu } from "react-icons/lu" + +import { menuDashboard } from "@/config/menu-dashboard" +import { siteConfig } from "@/config/site" +import { cn } from "@/lib/utils" +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion" +import { Button } from "@/components/ui/button" +import { ScrollArea } from "@/components/ui/scroll-area" +import { Separator } from "@/components/ui/separator" +import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet" +import { LightDarkImage } from "@/components/shared/light-dark-image" + +import { ModeToggle } from "../shared/mode-toggle" + +export function MobileNav() { + const [open, setOpen] = useState(false) + + return ( + +
    + + + + {siteConfig.name} + + + + + +
    + +
    + + + + +
    + +
    + + + + Integrations + + +
      + {integrationCategories.map((category) => { + const categoryIntegrations = Object.values( + turboIntegrations + ).filter( + (integration) => integration.category === category + ) + return ( + categoryIntegrations.length > 0 && ( + <> +

      + {category.charAt(0).toUpperCase() + + category.slice(1)} +

      + + {categoryIntegrations.map( + ({ name, href, imgDark, imgLight }) => ( + + ) + )} + + ) + ) + })} +
    +
    +
    + + + Dashboard + + +
    + {menuDashboard?.map((item, index) => + item.href ? ( + setOpen(false)} + > + {item.label} + + ) : ( +
    + {item.label} +
    + ) + )} +
    +
    +
    +
    + + Documentation + + +
    +
    +
    +
    + ) +} + +interface MobileLinkProps extends LinkProps { + onOpenChange?: (open: boolean) => void + children: React.ReactNode + className?: string +} + +function MobileLink({ + href, + onOpenChange, + className, + children, + ...props +}: MobileLinkProps) { + const router = useRouter() + return ( + { + router.push(href.toString()) + onOpenChange?.(false) + }} + className={cn(className)} + {...props} + > + {children} + + ) +} + +interface NavMenuListItemProps { + name: string + href: string + lightImage: string + darkImage: string + onOpenChange?: (open: boolean) => void +} + +const NavMenuListItem = ({ + name, + href, + lightImage, + darkImage, + onOpenChange, +}: NavMenuListItemProps) => { + return ( +
  • + +
    + + {name} +
    +
    +
  • + ) +} diff --git a/template/base/components/layout/navigation-menu-general.tsx b/template/base/components/layout/navigation-menu-general.tsx deleted file mode 100644 index 2eaa087..0000000 --- a/template/base/components/layout/navigation-menu-general.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { HTMLAttributes } from 'react' - -import Image from 'next/image' - -import { - NavigationMenu, - NavigationMenuContent, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - NavigationMenuTrigger, - navigationMenuTriggerStyle, -} from '@/components/ui/navigation-menu' -import { turboIntegrations } from '@/data/turbo-integrations' -import { cn } from '@/lib/utils' - -import { IsDarkTheme } from '../shared/is-dark-theme' -import { IsLightTheme } from '../shared/is-light-theme' -import { LinkComponent } from '../shared/link-component' - -export function NavigationMenuGeneral() { - return ( - - - - Features - -
      -
    • - -
      -
      -
      -

      - ⚡️ -
      - Build in Turbo Mode -

      -

      - TurboETH is a Web3 App Template built using Next.js, RainbowKit, SIWE, Disco, and more! -

      -

      #TurboETH

      -
      -
      - -
    • -
    • - -
      -

      🎛️ Dashboard

      -
      -

      - The TurboETH Dashboard is a great place to start. It's where you can see your app's features, and get started. -

      -
      - - -
      -

      🔐 Admin

      -
      -

      The TurboETH Admin area is where you can see your app's users.

      -
      - -
    • -
    -
    -
    - - Integrations - -
      - {Object.values(turboIntegrations).map((component) => ( - - {component.description} - - ))} -
    -
    -
    - - - Documentation - - -
    -
    - ) -} - -interface ListItemProps extends HTMLAttributes { - href: string - name: string - imgLight: string - imgDark: string -} - -const ListItem = ({ className, href, name, imgLight, imgDark, children, ...props }: ListItemProps) => { - return ( -
  • - - - - Etherscan logo - - - Etherscan logo - -
    {name}
    -

    {children}

    -
    -
    -
  • - ) -} -ListItem.displayName = 'ListItem' diff --git a/template/base/components/layout/page-header.tsx b/template/base/components/layout/page-header.tsx new file mode 100644 index 0000000..65e2094 --- /dev/null +++ b/template/base/components/layout/page-header.tsx @@ -0,0 +1,85 @@ +"use client" + +import { motion, MotionProps } from "framer-motion" +import Balancer from "react-wrap-balancer" + +import { cn } from "@/lib/utils" +import { fadeDownVariant, staggerContainer } from "@/lib/utils/motion" + +interface MotionHeaderProps extends MotionProps { + className?: string + children?: React.ReactNode +} + +function PageHeader({ className, children, ...props }: MotionHeaderProps) { + return ( + + {children} + + ) +} + +function PageHeaderHeading({ + className, + children, + ...props +}: MotionHeaderProps) { + return ( + + {children} + + ) +} + +function PageHeaderDescription({ + className, + children, + ...props +}: MotionHeaderProps) { + return ( + + {children} + + ) +} + +function PageHeaderCTA({ className, children, ...props }: MotionHeaderProps) { + return ( + + {children} + + ) +} + +export { PageHeader, PageHeaderHeading, PageHeaderDescription, PageHeaderCTA } diff --git a/template/base/components/layout/page-section.tsx b/template/base/components/layout/page-section.tsx new file mode 100644 index 0000000..2ae8248 --- /dev/null +++ b/template/base/components/layout/page-section.tsx @@ -0,0 +1,49 @@ +"use client" + +import { motion, MotionProps } from "framer-motion" + +import { cn } from "@/lib/utils" +import { staggerContainer } from "@/lib/utils/motion" + +interface MotionSectionProps extends MotionProps { + className?: string + children?: React.ReactNode +} + +function PageSection({ className, children, ...props }: MotionSectionProps) { + return ( + + {children} + + ) +} + +function PageSectionGrid({ className, children, ...prop }: MotionSectionProps) { + return ( + + {children} + + ) +} + +export { PageSection, PageSectionGrid } diff --git a/template/base/components/layout/sidebar-nav.tsx b/template/base/components/layout/sidebar-nav.tsx new file mode 100644 index 0000000..02cc63f --- /dev/null +++ b/template/base/components/layout/sidebar-nav.tsx @@ -0,0 +1,47 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" + +import { cn } from "@/lib/utils" + +export interface SidebarNavProps extends React.HTMLAttributes { + items: { + label: string + href?: string + }[] +} + +export function SidebarNav({ items, className, ...props }: SidebarNavProps) { + const pathname = usePathname() + + if (!items?.length) return null + + return ( +
    + {items.map((item, index) => { + return item.href ? ( + + + {item.label} + + + ) : ( + + {item.label} + + ) + })} +
    + ) +} diff --git a/template/base/components/layout/site-header.tsx b/template/base/components/layout/site-header.tsx new file mode 100644 index 0000000..45c39cc --- /dev/null +++ b/template/base/components/layout/site-header.tsx @@ -0,0 +1,37 @@ +"use client" + +import Link from "next/link" + +import useScroll from "@/lib/hooks/use-scroll" +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { MainNav } from "@/components/layout/main-nav" +import { MobileNav } from "@/components/layout/mobile-nav" +import { ModeToggle } from "@/components/shared/mode-toggle" + +export function SiteHeader() { + const scrolled = useScroll(0) + + return ( +
    +
    + + +
    + + Dashboard + + +
    +
    +
    + ) +} diff --git a/template/base/components/layout/user-dropdown.tsx b/template/base/components/layout/user-dropdown.tsx deleted file mode 100644 index 7fa3e00..0000000 --- a/template/base/components/layout/user-dropdown.tsx +++ /dev/null @@ -1,54 +0,0 @@ -'use client' -import { motion } from 'framer-motion' -import { LuBinary, LuDatabase, LuLayoutDashboard, LuLogOut } from 'react-icons/lu' - -import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' -import { FADE_IN_ANIMATION_SETTINGS } from '@/config/design' -import { IsSignedIn } from '@/integrations/siwe/components/is-signed-in' -import { IsSignedOut } from '@/integrations/siwe/components/is-signed-out' - -import { ButtonSIWELogin } from '../../integrations/siwe/components/button-siwe-login' -import { ButtonSIWELogout } from '../../integrations/siwe/components/button-siwe-logout' -import { LinkComponent } from '../shared/link-component' - -export function UserDropdown() { - return ( - - - - - - -
    - - -

    Site

    -
    - - -

    Dashboard

    -
    - - -

    Admin

    -
    - - - - Logout - - - - - - Login - - -
    -
    -
    -
    - ) -} diff --git a/template/base/components/providers/rainbow-kit.tsx b/template/base/components/providers/rainbow-kit.tsx index 58f3492..0bb9f52 100644 --- a/template/base/components/providers/rainbow-kit.tsx +++ b/template/base/components/providers/rainbow-kit.tsx @@ -1,19 +1,30 @@ -'use client' -import '@rainbow-me/rainbowkit/styles.css' -import { ReactNode } from 'react' +"use client" -import { RainbowKitProvider, darkTheme, lightTheme } from '@rainbow-me/rainbowkit' -import { connectorsForWallets } from '@rainbow-me/rainbowkit' -import { coinbaseWallet, injectedWallet, metaMaskWallet, rainbowWallet, walletConnectWallet } from '@rainbow-me/rainbowkit/wallets' -import { WagmiConfig, createConfig } from 'wagmi' +import "@rainbow-me/rainbowkit/styles.css" -import { chains, publicClient, webSocketPublicClient } from '@/config/networks' -import { siteConfig } from '@/config/site' -import { useColorMode } from '@/lib/state/color-mode' +import { ReactNode } from "react" +import { + connectorsForWallets, + darkTheme, + lightTheme, + RainbowKitProvider, +} from "@rainbow-me/rainbowkit" +import { + coinbaseWallet, + injectedWallet, + metaMaskWallet, + rainbowWallet, + walletConnectWallet, +} from "@rainbow-me/rainbowkit/wallets" +import { createConfig, WagmiConfig } from "wagmi" + +import { chains, publicClient, webSocketPublicClient } from "@/config/networks" +import { siteConfig } from "@/config/site" +import { useColorMode } from "@/lib/state/color-mode" const connectors = connectorsForWallets([ { - groupName: 'Recommended', + groupName: "Recommended", wallets: [ injectedWallet({ chains }), metaMaskWallet({ chains }), @@ -35,7 +46,10 @@ export function RainbowKit({ children }: { children: ReactNode }) { const [colorMode] = useColorMode() return ( - + {children} diff --git a/template/base/components/providers/root-provider.tsx b/template/base/components/providers/root-provider.tsx index a67c08c..1730bdf 100644 --- a/template/base/components/providers/root-provider.tsx +++ b/template/base/components/providers/root-provider.tsx @@ -1,14 +1,13 @@ -'use client' +"use client" -import { ReactNode } from 'react' +import { ReactNode } from "react" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { ThemeProvider } from "next-themes" +import { Provider as RWBProvider } from "react-wrap-balancer" -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { ThemeProvider } from 'next-themes' -import { Provider as RWBProvider } from 'react-wrap-balancer' - -import HandleWalletEvents from '@/components/blockchain/handle-wallet-events' -import { RainbowKit } from '@/components/providers/rainbow-kit' -import { useIsMounted } from '@/lib/hooks/use-is-mounted' +import { useIsMounted } from "@/lib/hooks/use-is-mounted" +import HandleWalletEvents from "@/components/blockchain/handle-wallet-events" +import { RainbowKit } from "@/components/providers/rainbow-kit" const queryClient = new QueryClient() interface RootProviderProps { @@ -18,7 +17,12 @@ interface RootProviderProps { export default function RootProvider({ children }: RootProviderProps) { const isMounted = useIsMounted() return isMounted ? ( - + diff --git a/template/base/components/shared/card.tsx b/template/base/components/shared/card.tsx deleted file mode 100644 index 3dbed0a..0000000 --- a/template/base/components/shared/card.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { ReactNode } from 'react' - -import { motion } from 'framer-motion' -import ReactMarkdown from 'react-markdown' -import Balancer from 'react-wrap-balancer' - -import { FADE_UP_ANIMATION_VARIANTS } from '@/config/design' - -import { LinkComponent } from './link-component' - -export default function Card({ - title, - description, - href, - demo, - large, -}: { - title: string - description: string - demo: ReactNode - large?: boolean - href?: string -}) { - return ( - -
    {demo}
    - -
    - ) -} diff --git a/template/base/components/shared/copy-button.tsx b/template/base/components/shared/copy-button.tsx new file mode 100644 index 0000000..4dc1410 --- /dev/null +++ b/template/base/components/shared/copy-button.tsx @@ -0,0 +1,55 @@ +"use client" + +import { useEffect, useState } from "react" +import { LuCheck, LuCopy } from "react-icons/lu" + +import { cn } from "@/lib/utils" +import { Button } from "@/components/ui/button" + +interface CopyButtonProps extends React.HTMLAttributes { + value: string + children: React.ReactNode + src?: string +} + +export async function copyToClipboard(value: string) { + await navigator.clipboard.writeText(value) +} + +export function CopyButton({ + value, + className, + children, + src, + ...props +}: CopyButtonProps) { + const [hasCopied, setHasCopied] = useState(false) + + useEffect(() => { + setTimeout(() => { + setHasCopied(false) + }, 3000) + }, [hasCopied]) + + return ( + + ) +} diff --git a/template/base/components/shared/example-demos.tsx b/template/base/components/shared/example-demos.tsx new file mode 100644 index 0000000..2ea6bbf --- /dev/null +++ b/template/base/components/shared/example-demos.tsx @@ -0,0 +1,600 @@ +"use client" + +import Image from "next/image" +import Link from "next/link" +import { turboIntegrations } from "@/data/turbo-integrations" +import { motion, MotionProps } from "framer-motion" +import ReactMarkdown from "react-markdown" +import Balancer from "react-wrap-balancer" + +import { DEPLOY_URL } from "@/config/site" +import { cn } from "@/lib/utils" +import { fadeUpVariant } from "@/lib/utils/motion" +import { buttonVariants } from "@/components/ui/button" +import { WalletAddress } from "@/components/blockchain/wallet-address" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { PageSectionGrid } from "@/components/layout/page-section" +import { IsDarkTheme } from "@/components/shared/is-dark-theme" +import { IsLightTheme } from "@/components/shared/is-light-theme" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { LightDarkImage } from "@/components/shared/light-dark-image" +import { + ERC20Decimals, + ERC20Name, + ERC20Symbol, +} from "@/integrations/erc20/components/erc20-read" +import { ERC721TokenUriImage, ERC721TokenUriName } from "@/integrations/erc721" +import { ButtonSIWELogin } from "@/integrations/siwe/components/button-siwe-login" +import { ButtonSIWELogout } from "@/integrations/siwe/components/button-siwe-logout" +import { IsSignedIn } from "@/integrations/siwe/components/is-signed-in" +import { IsSignedOut } from "@/integrations/siwe/components/is-signed-out" + +const demos = [ + { + title: "Web3 Components for the power developer", + description: "Pre-built Web3 components, powered by WAGMI", + large: true, + demo: ( +
    + +
    +
    + + + <WalletAddress isLink truncate /> + +
    +
    +
    + + + +
    + ), + }, + { + title: "One-click Deploy", + description: + "Start your next Web3 project in ⚡ Turbo Mode with a deploy to [Vercel](https://vercel.com/) in one click.", + demo: ( +
    + Deploy with Vercel + + ), + }, + { + title: turboIntegrations.disco.name, + description: turboIntegrations.disco.description, + href: turboIntegrations.disco.href, + demo: ( +
    + Disco logo +
    + ), + }, + { + title: "Sign-In With Ethereum", + description: turboIntegrations.siwe.description, + href: turboIntegrations.siwe.href, + demo: ( +
    + Prisma logo +
    + ), + }, + { + title: "Rainbowkit", + description: + "The best way to connect a wallet. Designed for everyone. Built for developers.", + demo: ( +
    + Rainbow logo +
    + ), + }, + { + title: turboIntegrations.etherscan.name, + description: turboIntegrations.etherscan.description, + href: turboIntegrations.etherscan.href, + demo: ( +
    + + Etherscan logo + + + Etherscan logo + +
    + ), + }, + { + title: "Web3 Login", + description: "Authenticate using an Ethereum Account", + demo: ( +
    + + + + + + + + + + + +
    + ), + }, + { + title: "ERC20 WAGMI", + description: + "Read and Write to ERC20 smart contracts using minimal UI components.", + demo: ( +
    + {`Token +

    + {" "} + ( + + ) +

    +

    + Decimals{" "} + +

    + + View Token Page + +
    + ), + }, + { + title: "ERC721 WAGMI", + description: + "Read and Write to ERC721 smart contracts using minimal UI components.", + demo: ( +
    + + + + View Token Page + +
    + ), + }, + { + title: turboIntegrations.sessionKeys.name, + description: turboIntegrations.sessionKeys.description, + href: turboIntegrations.sessionKeys.href, + demo: ( +
    + + Session keys logo + + + Session keys logo + +
    + ), + }, + { + title: turboIntegrations.litProtocol.name, + description: turboIntegrations.litProtocol.description, + href: turboIntegrations.litProtocol.href, + demo: ( +
    + + Lit Protocol logo + + + Lit Protocol logo + +
    + ), + }, + { + title: turboIntegrations.openai.name, + description: turboIntegrations.openai.description, + href: turboIntegrations.openai.href, + demo: ( +
    + + OpenAI logo + + + OpenAI logo + +
    + ), + }, + { + title: turboIntegrations.pooltogether_v4.name, + description: turboIntegrations.pooltogether_v4.description, + href: turboIntegrations.pooltogether_v4.href, + demo: ( +
    + + PoolTogether logo + + + PoolTogether logo + +
    + ), + }, + { + title: turboIntegrations.livepeer.name, + description: turboIntegrations.livepeer.description, + href: turboIntegrations.livepeer.href, + demo: ( +
    + + Livepeer logo + + + Livepeer logo + +
    + ), + }, + { + title: turboIntegrations.connext.name, + description: turboIntegrations.connext.description, + href: turboIntegrations.connext.href, + demo: ( +
    + + {`${turboIntegrations.connext.name} + + + {`${turboIntegrations.connext.name} + +
    + ), + }, + { + title: turboIntegrations.gelato.name, + description: turboIntegrations.gelato.description, + href: turboIntegrations.gelato.href, + demo: ( +
    + + {`${turboIntegrations.gelato.name} + + + {`${turboIntegrations.gelato.name} + +
    + ), + }, + { + title: turboIntegrations.push_protocol.name, + description: turboIntegrations.push_protocol.description, + href: turboIntegrations.push_protocol.href, + demo: ( +
    + + Push Protocol logo + + + Push Protocol logo + +
    + ), + }, + { + title: turboIntegrations.moralis.name, + description: turboIntegrations.moralis.description, + href: turboIntegrations.moralis.href, + demo: ( +
    + +
    + ), + }, + { + title: turboIntegrations.aave.name, + description: turboIntegrations.aave.description, + href: turboIntegrations.aave.href, + demo: ( +
    + +
    + ), + }, + { + title: turboIntegrations.arweave.name, + description: turboIntegrations.arweave.description, + href: turboIntegrations.arweave.href, + demo: ( +
    + +
    + ), + }, + { + title: turboIntegrations.gitcoinPassport.name, + description: turboIntegrations.gitcoinPassport.description, + href: turboIntegrations.gitcoinPassport.href, + demo: ( +
    + +
    + ), + }, + { + title: turboIntegrations.lensProtocol.name, + description: turboIntegrations.lensProtocol.description, + href: turboIntegrations.lensProtocol.href, + demo: ( +
    + +
    + ), + }, + { + title: turboIntegrations.starter.name, + description: turboIntegrations.starter.description, + href: turboIntegrations.starter.href, + demo: ( +
    + +
    + ), + }, +] + +interface ExampleDemosProps extends MotionProps { + className?: string +} + +export function ExampleDemos({ className, ...props }: ExampleDemosProps) { + return ( + + {demos.map(({ title, description, href, demo, large }) => ( + + ))} + + ) +} + +interface DemoCardProps extends MotionProps { + demo: React.ReactNode + title: string + description: string + large?: boolean + href?: string +} + +function DemoCard({ title, description, href, demo, large }: DemoCardProps) { + return ( + +
    {demo}
    + +
    + ) +} diff --git a/template/base/components/shared/icons.tsx b/template/base/components/shared/icons.tsx deleted file mode 100644 index 27fa1ba..0000000 --- a/template/base/components/shared/icons.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { IconBaseProps, IconType } from 'react-icons' -import { LuLaptop, LuMoon, LuSunMedium, LuTwitter } from 'react-icons/lu' - -export type Icon = IconType - -export const Icons = { - sun: LuSunMedium, - moon: LuMoon, - laptop: LuLaptop, - twitter: LuTwitter, - logo: (props: IconBaseProps) => ( - - - - ), - gitHub: (props: IconBaseProps) => ( - - - - ), -} diff --git a/template/base/components/shared/is-dark-theme.tsx b/template/base/components/shared/is-dark-theme.tsx index 796c032..c773ddd 100644 --- a/template/base/components/shared/is-dark-theme.tsx +++ b/template/base/components/shared/is-dark-theme.tsx @@ -1,17 +1,16 @@ -'use client' +"use client" -import { ReactNode } from 'react' - -import { useColorMode } from '@/lib/state/color-mode' +import { ReactNode } from "react" +import { useTheme } from "next-themes" interface IsDarkThemeProps { children: ReactNode } export const IsDarkTheme = ({ children }: IsDarkThemeProps) => { - const [colorMode] = useColorMode() + const { resolvedTheme } = useTheme() - if (colorMode !== 'light') return <>{children} + if (resolvedTheme === "dark") return <>{children} return null } diff --git a/template/base/components/shared/is-desktop.tsx b/template/base/components/shared/is-desktop.tsx deleted file mode 100644 index a06a97e..0000000 --- a/template/base/components/shared/is-desktop.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client' - -import { ReactNode } from 'react' - -import { useMediaQuery } from 'react-responsive' - -interface IsDesktopProps { - children: ReactNode -} - -export const IsDesktop = ({ children }: IsDesktopProps) => { - const isTabletOrMobile = useMediaQuery({ query: '(max-width: 1224px)' }) - - if (!isTabletOrMobile) return <>{children} - - return null -} diff --git a/template/base/components/shared/is-light-theme.tsx b/template/base/components/shared/is-light-theme.tsx index 3a5cfdd..3938a91 100644 --- a/template/base/components/shared/is-light-theme.tsx +++ b/template/base/components/shared/is-light-theme.tsx @@ -1,17 +1,16 @@ -'use client' +"use client" -import { ReactNode } from 'react' - -import { useColorMode } from '@/lib/state/color-mode' +import { ReactNode } from "react" +import { useTheme } from "next-themes" interface IsLightThemeProps { children: ReactNode } export const IsLightTheme = ({ children }: IsLightThemeProps) => { - const [colorMode] = useColorMode() + const { resolvedTheme } = useTheme() - if (colorMode === 'light') return <>{children} + if (resolvedTheme === "light") return <>{children} return null } diff --git a/template/base/components/shared/is-mobile.tsx b/template/base/components/shared/is-mobile.tsx deleted file mode 100644 index aac5b69..0000000 --- a/template/base/components/shared/is-mobile.tsx +++ /dev/null @@ -1,17 +0,0 @@ -'use client' - -import { ReactNode } from 'react' - -import { useMediaQuery } from 'react-responsive' - -interface IsMobileProps { - children: ReactNode -} - -export const IsMobile = ({ children }: IsMobileProps) => { - const isTabletOrMobile = useMediaQuery({ query: '(max-width: 1224px)' }) - - if (isTabletOrMobile) return <>{children} - - return null -} diff --git a/template/base/components/shared/is-wallet-connected.tsx b/template/base/components/shared/is-wallet-connected.tsx index a778ab1..ef05b4b 100644 --- a/template/base/components/shared/is-wallet-connected.tsx +++ b/template/base/components/shared/is-wallet-connected.tsx @@ -1,8 +1,7 @@ -'use client' +"use client" -import { ReactNode } from 'react' - -import { useAccount } from 'wagmi' +import { ReactNode } from "react" +import { useAccount } from "wagmi" interface IsWalletConnectedProps { children: ReactNode diff --git a/template/base/components/shared/is-wallet-disconnected.tsx b/template/base/components/shared/is-wallet-disconnected.tsx index 57b718d..45907ab 100644 --- a/template/base/components/shared/is-wallet-disconnected.tsx +++ b/template/base/components/shared/is-wallet-disconnected.tsx @@ -1,14 +1,15 @@ -'use client' +"use client" -import { ReactNode } from 'react' - -import { useAccount } from 'wagmi' +import { ReactNode } from "react" +import { useAccount } from "wagmi" interface IsWalletDisconnectedProps { children: ReactNode } -export const IsWalletDisconnected = ({ children }: IsWalletDisconnectedProps) => { +export const IsWalletDisconnected = ({ + children, +}: IsWalletDisconnectedProps) => { const { address } = useAccount() if (!address) return <>{children} diff --git a/template/base/components/shared/light-dark-image.tsx b/template/base/components/shared/light-dark-image.tsx new file mode 100644 index 0000000..2989307 --- /dev/null +++ b/template/base/components/shared/light-dark-image.tsx @@ -0,0 +1,42 @@ +"use client" + +import Image from "next/image" +import { useTheme } from "next-themes" + +import { cn } from "@/lib/utils" + +interface LightDarkImageProps { + LightImage: string + DarkImage: string + alt: string + height: number + width: number + className?: string +} +const LightDarkImage = ({ + LightImage, + DarkImage, + alt, + height, + width, + className, +}: LightDarkImageProps) => { + const { resolvedTheme } = useTheme() + + let imageUrl = LightImage + if (resolvedTheme === "dark") { + imageUrl = DarkImage + } + + return ( + {alt} + ) +} + +export { LightDarkImage } diff --git a/template/base/components/shared/link-component.tsx b/template/base/components/shared/link-component.tsx index 3ded29e..db7438b 100644 --- a/template/base/components/shared/link-component.tsx +++ b/template/base/components/shared/link-component.tsx @@ -1,11 +1,10 @@ -'use client' +"use client" -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import Link from "next/link" +import { usePathname } from "next/navigation" -import Link from 'next/link' -import { usePathname } from 'next/navigation' - -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" interface LinkComponentProps extends HTMLAttributes { href: string @@ -13,16 +12,30 @@ interface LinkComponentProps extends HTMLAttributes { target?: string } -export function LinkComponent({ href, children, isExternal, className, target = '_blank', ...props }: LinkComponentProps) { +export function LinkComponent({ + href, + children, + isExternal, + className, + target = "_blank", + ...props +}: LinkComponentProps) { const pathname = usePathname() const classes = cn(className, { active: pathname === href, }) - const isExternalEnabled = href.match(/^([a-z0-9]*:|.{0})\/\/.*$/) || isExternal + const isExternalEnabled = + href.match(/^([a-z0-9]*:|.{0})\/\/.*$/) || isExternal if (isExternalEnabled) { return ( - + {children} ) diff --git a/template/base/components/shared/mode-toggle.tsx b/template/base/components/shared/mode-toggle.tsx new file mode 100644 index 0000000..187b176 --- /dev/null +++ b/template/base/components/shared/mode-toggle.tsx @@ -0,0 +1,41 @@ +"use client" + +import { useTheme } from "next-themes" +import { LuLaptop, LuMoon, LuSun } from "react-icons/lu" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export function ModeToggle() { + const { setTheme } = useTheme() + return ( + + + + + + setTheme("light")}> + + Light + + setTheme("dark")}> + + Dark + + setTheme("system")}> + + System + + + + ) +} diff --git a/template/base/components/shared/table/table-body.tsx b/template/base/components/shared/table/table-body.tsx index 0c915f7..66bca11 100644 --- a/template/base/components/shared/table/table-body.tsx +++ b/template/base/components/shared/table/table-body.tsx @@ -1,7 +1,7 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" +import { TableBodyProps } from "react-table" -import { cn } from '@/lib/utils' -import { TableBodyProps } from 'react-table' +import { cn } from "@/lib/utils" interface ITableBody extends HTMLAttributes { page: Array @@ -13,22 +13,30 @@ interface ITableBody extends HTMLAttributes { * @name TableBody * @param {Object} props */ -export const TableBody = ({ className, page, prepareRow, ...props }: ITableBody) => { - const styleCell = cn(className, 'border-b-2 border-gray-100 dark:border-neutral-700 px-4 py-3') +export const TableBody = ({ + className, + page, + prepareRow, + ...props +}: ITableBody) => { + const styleCell = cn( + className, + "border-b-2 border-gray-100 dark:border-neutral-700 px-4 py-3" + ) return ( - + {page.map((row, idx) => { prepareRow(row) - const styleRow = cn('row py-3', { - 'bg-gray-100 text-gray-500 dark:text-white': row.original.disabled, - 'bg-white dark:bg-neutral-800 dark:text-white': !row.original.disabled, + const styleRow = cn("row py-3", { + "bg-card text-muted-foreground": row.original.disabled, + "bg-card": !row.original.disabled, }) return ( {row.cells.map((cell: any, cIdx: number) => { return ( - {cell.render('Cell')} + {cell.render("Cell")} ) })} diff --git a/template/base/components/shared/table/table-core.tsx b/template/base/components/shared/table/table-core.tsx index 7e66de2..9e1dde1 100644 --- a/template/base/components/shared/table/table-core.tsx +++ b/template/base/components/shared/table/table-core.tsx @@ -1,18 +1,18 @@ -import { HTMLAttributes } from 'react' +import { HTMLAttributes } from "react" import { TableInstance, - UsePaginationInstanceProps, - UsePaginationState, - UseSortByInstanceProps, useExpanded, usePagination, + UsePaginationInstanceProps, + UsePaginationState, useSortBy, + UseSortByInstanceProps, useTable, -} from 'react-table' +} from "react-table" -import TableBody from './table-body' -import TableHead from './table-head' -import TablePagination from './table-pagination' +import TableBody from "./table-body" +import TableHead from "./table-head" +import TablePagination from "./table-pagination" interface TableProps extends HTMLAttributes { data: Array @@ -55,8 +55,16 @@ export function TableCore({ className, columns, data, ...props }: TableProps) { return (
    - - + +
    { headerGroups: any defaultStyle: boolean } -export const TableHead = ({ className, headerGroups, defaultStyle, ...props }: ITableHead) => { +export const TableHead = ({ + className, + headerGroups, + defaultStyle, + ...props +}: ITableHead) => { const styleBase = cn(className, { - 'rounded-xl shadow-sm border-b-2 border-blue-300 pb-5 h-20 z-10': defaultStyle, + "rounded-xl shadow-sm border-b-2 border-blue-300 pb-5 h-20 z-10 bg-card rounded-t-xl": + defaultStyle, }) return ( {headerGroups.map((headerGroup: any, ihg: any) => ( {headerGroup.headers.map((column: any, idx: number) => ( - - {column.render('Header')} - {column.isSorted ? (column.isSortedDesc ? ' 🔽' : ' 🔼') : ''} + + {column.render("Header")} + + {column.isSorted ? (column.isSortedDesc ? " 🔽" : " 🔼") : ""} + ))} diff --git a/template/base/components/shared/table/table-pagination.tsx b/template/base/components/shared/table/table-pagination.tsx index 63d0d0b..3bc1f08 100644 --- a/template/base/components/shared/table/table-pagination.tsx +++ b/template/base/components/shared/table/table-pagination.tsx @@ -1,4 +1,6 @@ -import { ReactElement } from 'react' +import { ReactElement } from "react" + +import { Input } from "@/components/ui/input" interface ITablePagination { canPreviousPage: boolean @@ -28,32 +30,37 @@ export const TablePagination = ({ setPageSize, }: ITablePagination): ReactElement => { return ( -
    -
    - {' '} - {' '} - {' '} - {' '} - - Page{' '} - - {pageIndex + 1} of {pageCount} - {' '} - +
    +
    +
    + {" "} + {" "} + {" "} + {" "} + + Page{" "} + + {pageIndex + 1} of {pageCount} + {" "} + +
    | - Go to page:{' '} - - {' '} -
    -
    - + {" "}
    +
    ) } diff --git a/template/base/components/shared/theme-toggle.tsx b/template/base/components/shared/theme-toggle.tsx deleted file mode 100644 index cbbf564..0000000 --- a/template/base/components/shared/theme-toggle.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect } from 'react' - -import { useTheme } from 'next-themes' - -import { Icons } from '@/components/shared/icons' -import { Button } from '@/components/ui/button' -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import { useColorMode } from '@/lib/state/color-mode' - -import { IsDarkTheme } from './is-dark-theme' -import { IsLightTheme } from './is-light-theme' - -export function ThemeToggle() { - const { setTheme } = useTheme() - const [colorMode, toggleMode, setMode] = useColorMode() - - const handleSetLightTheme = () => { - setTheme('light') - setMode('light') - } - - const handleSetDarkTheme = () => { - setTheme('dark') - setMode('dark') - } - - const handleSetSystemTheme = () => { - setTheme('system') - setMode('system') - } - - useEffect(() => { - colorMode === 'system' ? setTheme('system') : colorMode === 'dark' ? setTheme('dark') : setTheme('light') - }, [colorMode]) - - return ( - - - - - - - - Light - - - - Dark - - - - System - - - - ) -} diff --git a/template/base/components/shared/time-from-epoch.tsx b/template/base/components/shared/time-from-epoch.tsx index b75fdb7..3753225 100644 --- a/template/base/components/shared/time-from-epoch.tsx +++ b/template/base/components/shared/time-from-epoch.tsx @@ -1,16 +1,23 @@ -import { HTMLAttributes, useEffect, useState } from 'react' - -import { DateTime } from 'luxon' +import { HTMLAttributes, useEffect, useState } from "react" +import { DateTime } from "luxon" interface TimeFromEpochProps extends HTMLAttributes { epoch?: number | string } -export const TimeFromEpoch = ({ className, epoch, ...props }: TimeFromEpochProps) => { +export const TimeFromEpoch = ({ + className, + epoch, + ...props +}: TimeFromEpochProps) => { const [timestamp, setTimestamp] = useState() useEffect(() => { if (epoch) { - setTimestamp(DateTime.fromSeconds(Number(epoch)).toLocaleString(DateTime.DATETIME_MED)) + setTimestamp( + DateTime.fromSeconds(Number(epoch)).toLocaleString( + DateTime.DATETIME_MED + ) + ) } }, []) return ( diff --git a/template/base/components/shared/time-from-utc.tsx b/template/base/components/shared/time-from-utc.tsx index f256dcf..15105a8 100644 --- a/template/base/components/shared/time-from-utc.tsx +++ b/template/base/components/shared/time-from-utc.tsx @@ -1,12 +1,15 @@ -import { HTMLAttributes, useEffect, useState } from 'react' - -import { DateTime } from 'luxon' +import { HTMLAttributes, useEffect, useState } from "react" +import { DateTime } from "luxon" interface TimeFromUtcProps extends HTMLAttributes { date: string } -export const TimeFromUtc = ({ className, date, ...props }: TimeFromUtcProps) => { +export const TimeFromUtc = ({ + className, + date, + ...props +}: TimeFromUtcProps) => { const [timestamp, setTimestamp] = useState() useEffect(() => { if (date) { diff --git a/template/base/components/ui/accordion.tsx b/template/base/components/ui/accordion.tsx index f77e467..d23cf3d 100644 --- a/template/base/components/ui/accordion.tsx +++ b/template/base/components/ui/accordion.tsx @@ -1,50 +1,60 @@ -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' +"use client" -import * as AccordionPrimitive from '@radix-ui/react-accordion' -import { LuChevronDown } from 'react-icons/lu' +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { LuChevronDown } from "react-icons/lu" -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" const Accordion = AccordionPrimitive.Root -const AccordionItem = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -) -AccordionItem.displayName = 'AccordionItem' +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" -const AccordionTrigger = forwardRef, ComponentPropsWithoutRef>( - ({ className, children, ...props }, ref) => ( - - svg]:rotate-180', - className - )} - {...props}> - {children} - - - - ) -) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName - -const AccordionContent = forwardRef, ComponentPropsWithoutRef>( - ({ className, children, ...props }, ref) => ( - , + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", className )} - {...props}> -
    {children}
    -
    - ) -) + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
    {children}
    +
    +)) AccordionContent.displayName = AccordionPrimitive.Content.displayName export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/template/base/components/ui/alert-dialog.tsx b/template/base/components/ui/alert-dialog.tsx index 5be9389..ca3d874 100644 --- a/template/base/components/ui/alert-dialog.tsx +++ b/template/base/components/ui/alert-dialog.tsx @@ -1,102 +1,135 @@ -'use client' +"use client" -import { ComponentPropsWithoutRef, ElementRef, HTMLAttributes, forwardRef } from 'react' +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" -import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' - -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" const AlertDialog = AlertDialogPrimitive.Root const AlertDialogTrigger = AlertDialogPrimitive.Trigger -const AlertDialogPortal = ({ className, children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => ( - -
    {children}
    -
    +const AlertDialogPortal = ({ + className, + ...props +}: AlertDialogPrimitive.AlertDialogPortalProps) => ( + ) AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName -const AlertDialogOverlay = forwardRef, ComponentPropsWithoutRef>( - ({ className, children, ...props }, ref) => ( - - ) -) +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName -const AlertDialogContent = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - - - - ) -) +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName -const AlertDialogHeader = ({ className, ...props }: HTMLAttributes) => ( -
    +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    ) -AlertDialogHeader.displayName = 'AlertDialogHeader' +AlertDialogHeader.displayName = "AlertDialogHeader" -const AlertDialogFooter = ({ className, ...props }: HTMLAttributes) => ( -
    +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
    ) -AlertDialogFooter.displayName = 'AlertDialogFooter' +AlertDialogFooter.displayName = "AlertDialogFooter" -const AlertDialogTitle = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -) +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName -const AlertDialogDescription = forwardRef< - ElementRef, - ComponentPropsWithoutRef +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )) -AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName -const AlertDialogAction = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -) +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName -const AlertDialogCancel = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -) +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName export { diff --git a/template/base/components/ui/aspect-ratio.tsx b/template/base/components/ui/aspect-ratio.tsx deleted file mode 100644 index 07bc674..0000000 --- a/template/base/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' - -const AspectRatio = AspectRatioPrimitive.Root - -export { AspectRatio } diff --git a/template/base/components/ui/avatar.tsx b/template/base/components/ui/avatar.tsx index 90e4655..51e507b 100644 --- a/template/base/components/ui/avatar.tsx +++ b/template/base/components/ui/avatar.tsx @@ -1,30 +1,50 @@ -import { ComponentPropsWithoutRef, ElementRef, forwardRef } from 'react' +"use client" -import * as AvatarPrimivite from '@radix-ui/react-avatar' +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" -const Avatar = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -) -Avatar.displayName = AvatarPrimivite.Root.displayName +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName -const AvatarImage = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => -) -AvatarImage.displayName = AvatarPrimivite.Image.displayName +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName -const AvatarFallback = forwardRef, ComponentPropsWithoutRef>( - ({ className, ...props }, ref) => ( - - ) -) -AvatarFallback.displayName = AvatarPrimivite.Fallback.displayName +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName export { Avatar, AvatarImage, AvatarFallback } diff --git a/template/base/components/ui/badge.tsx b/template/base/components/ui/badge.tsx new file mode 100644 index 0000000..e87d62b --- /dev/null +++ b/template/base/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
    + ) +} + +export { Badge, badgeVariants } diff --git a/template/base/components/ui/button.tsx b/template/base/components/ui/button.tsx index c0ae227..857101c 100644 --- a/template/base/components/ui/button.tsx +++ b/template/base/components/ui/button.tsx @@ -1,39 +1,59 @@ -import { ButtonHTMLAttributes, forwardRef } from 'react' +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" -import { VariantProps, cva } from 'class-variance-authority' - -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" const buttonVariants = cva( - 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800', + "inline-flex items-center justify-center rounded-md text-base font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", { variants: { variant: { - default: 'bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900', - outline: 'bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100', - subtle: 'bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100', - ghost: - 'bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent', - link: 'bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent', + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + blue: "bg-blue-600 text-white shadow hover:bg-blue-600/90", + emerald: "bg-emerald-600 text-white shadow hover:bg-emerald-600/90", }, size: { - default: 'h-10 py-2 px-4', - sm: 'h-9 px-2 rounded-md', - lg: 'h-11 px-8 rounded-md', + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", }, }, defaultVariants: { - variant: 'default', - size: 'default', + variant: "default", + size: "default", }, } ) -export interface ButtonProps extends ButtonHTMLAttributes, VariantProps {} +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} -const Button = forwardRef(({ className, variant, size, ...props }, ref) => { - return +
    )} @@ -163,19 +219,29 @@ export function CreateTask() {
    Transaction Started
    -
    {moment(createTx.timestamp).format('ll, HH:mm:ss')}
    +
    + {moment(createTx.timestamp).format("ll, HH:mm:ss")} +
    )} - {createTaskIsError &&
    Error creating task, please try again
    } + {createTaskIsError && ( +
    + Error creating task, please try again +
    + )} ) diff --git a/template/integrations/gelato/core/gelato/components/create-task/execution-values.tsx b/template/integrations/gelato/core/gelato/components/create-task/execution-values.tsx index 57554b5..b37f6cf 100644 --- a/template/integrations/gelato/core/gelato/components/create-task/execution-values.tsx +++ b/template/integrations/gelato/core/gelato/components/create-task/execution-values.tsx @@ -1,21 +1,24 @@ -import { ChangeEvent, useMemo } from 'react' +import { ChangeEvent, useMemo } from "react" +import { Abi } from "abitype" +import { useFormContext } from "react-hook-form" -import { Abi } from 'abitype' -import { useFormContext } from 'react-hook-form' +import { cn } from "@/lib/utils" -import { cn } from '@/lib/utils' - -import { CreateTaskForm } from './create-task' -import { getAbiFunctions, validateInput } from '../../utils/helpers' -import { ValidationError } from '../errors/validation-error' +import { getAbiFunctions, validateInput } from "../../utils/helpers" +import { ValidationError } from "../errors/validation-error" +import { CreateTaskForm } from "./create-task" export type ExecutionValuesProps = { - inputFieldName: 'predefinedInputs' | 'resolverInputs' - funcFieldName: 'func' | 'resolverFunc' - abiFieldName: 'abi' | 'resolverAbi' + inputFieldName: "predefinedInputs" | "resolverInputs" + funcFieldName: "func" | "resolverFunc" + abiFieldName: "abi" | "resolverAbi" } -export function ExecutionValues({ inputFieldName, funcFieldName, abiFieldName }: ExecutionValuesProps) { +export function ExecutionValues({ + inputFieldName, + funcFieldName, + abiFieldName, +}: ExecutionValuesProps) { const { watch, setValue, @@ -23,16 +26,23 @@ export function ExecutionValues({ inputFieldName, funcFieldName, abiFieldName }: formState: { errors }, } = useFormContext() - const [inputDefinition, func, abi, predefinedInputs] = watch(['inputDefinition', funcFieldName, abiFieldName, inputFieldName]) + const [inputDefinition, func, abi, predefinedInputs] = watch([ + "inputDefinition", + funcFieldName, + abiFieldName, + inputFieldName, + ]) - const isResolver = funcFieldName === 'resolverFunc' - const shouldShowInputs = inputDefinition === 'predefined' || isResolver + const isResolver = funcFieldName === "resolverFunc" + const shouldShowInputs = inputDefinition === "predefined" || isResolver const selectedFunctionAbi = useMemo(() => { if (!abi) return try { - return getAbiFunctions(JSON.parse(abi) as Abi).find((item) => item.name === func) + return getAbiFunctions(JSON.parse(abi) as Abi).find( + (item) => item.name === func + ) } catch (e) { return } @@ -46,19 +56,21 @@ export function ExecutionValues({ inputFieldName, funcFieldName, abiFieldName }:
    @@ -79,7 +91,12 @@ export function ExecutionValues({ inputFieldName, funcFieldName, abiFieldName }: placeholder="value" {...register(`${inputFieldName}.${item.name}`, { validate: { - format: (value) => validateInput(item.name as string, value, selectedFunctionAbi), + format: (value) => + validateInput( + item.name as string, + value, + selectedFunctionAbi + ), }, onChange: (e: ChangeEvent) => { setValue(inputFieldName, { @@ -89,7 +106,9 @@ export function ExecutionValues({ inputFieldName, funcFieldName, abiFieldName }: }, })} /> - + )}
    diff --git a/template/integrations/gelato/core/gelato/components/create-task/function-input.tsx b/template/integrations/gelato/core/gelato/components/create-task/function-input.tsx index dfe26e3..30b14ef 100644 --- a/template/integrations/gelato/core/gelato/components/create-task/function-input.tsx +++ b/template/integrations/gelato/core/gelato/components/create-task/function-input.tsx @@ -1,19 +1,28 @@ -import { useMemo } from 'react' +import { useMemo } from "react" +import { Abi } from "abitype" +import { useFormContext } from "react-hook-form" -import { Abi } from 'abitype' -import { useFormContext } from 'react-hook-form' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' - -import { CreateTaskForm } from './create-task' -import { getAbiFunctions } from '../../utils/helpers' +import { getAbiFunctions } from "../../utils/helpers" +import { CreateTaskForm } from "./create-task" export type FunctionInputProps = { - abiFieldName: 'abi' | 'resolverAbi' - funcFieldName: 'func' | 'resolverFunc' - inputsFieldName: 'predefinedInputs' | 'resolverInputs' + abiFieldName: "abi" | "resolverAbi" + funcFieldName: "func" | "resolverFunc" + inputsFieldName: "predefinedInputs" | "resolverInputs" } -export function FunctionInput({ abiFieldName, funcFieldName, inputsFieldName }: FunctionInputProps) { +export function FunctionInput({ + abiFieldName, + funcFieldName, + inputsFieldName, +}: FunctionInputProps) { const { watch, setValue } = useFormContext() const [formAbi] = watch([abiFieldName]) @@ -31,28 +40,40 @@ export function FunctionInput({ abiFieldName, funcFieldName, inputsFieldName }: return (
    + {...register("timeInterval.days")} + > + + {...register("timeInterval.hours")} + > + + {...register("timeInterval.minutes")} + > + + {...register("timeInterval.seconds")} + >
    @@ -119,15 +143,26 @@ export function IntervalInput() {
    + type="datetime-local" + >
    - setValue('startImmediately', val)} /> -
    @@ -135,21 +170,34 @@ export function IntervalInput() {
    1st execution: - {startImmediately ? firstExecution : moment(startTime).format('ll, HH:mm:ss')} + + {startImmediately + ? firstExecution + : moment(startTime).format("ll, HH:mm:ss")} +
    2nd execution: - {startImmediately ? secondExecution : moment(startTime).add(totalInterval, 'seconds').format('ll, HH:mm:ss')} + + {startImmediately + ? secondExecution + : moment(startTime) + .add(totalInterval, "seconds") + .format("ll, HH:mm:ss")} +
    )}
    ) : (
    -
    Gelato will check if the function is executable at every block.
    +
    + Gelato will check if the function is executable at every block. +
    - Checking the function will start immediately after the task is created. Make sure the function can only be executed from time to time - and not always. + Checking the function will start immediately after the task is + created. Make sure the function can only be executed from time to + time and not always.
    )} diff --git a/template/integrations/gelato/core/gelato/components/create-task/payment-input.tsx b/template/integrations/gelato/core/gelato/components/create-task/payment-input.tsx index db58e34..ccf24a5 100644 --- a/template/integrations/gelato/core/gelato/components/create-task/payment-input.tsx +++ b/template/integrations/gelato/core/gelato/components/create-task/payment-input.tsx @@ -1,51 +1,67 @@ -import { useFormContext } from 'react-hook-form' +import { useFormContext } from "react-hook-form" -import { cn } from '@/lib/utils' +import { cn } from "@/lib/utils" -import { CreateTaskForm } from './create-task' -import { GELATO_CONSTANTS } from '../../utils/constants' +import { GELATO_CONSTANTS } from "../../utils/constants" +import { CreateTaskForm } from "./create-task" export function PaymentInput() { const { watch, setValue } = useFormContext() - const [payWith] = watch(['payWith']) + const [payWith] = watch(["payWith"]) return ( -
    +

    Pay with

    -
    Choose how the task should be paid for. The cost of each execution equals the network fee
    +
    - {payWith === 'transaction' && ( + {payWith === "transaction" && (
    - The fees will be taken from the address of the automated contract.{' '} - + The fees will be taken from the address of the automated contract.{" "} + Docs
    diff --git a/template/integrations/gelato/core/gelato/components/create-task/resolver-input.tsx b/template/integrations/gelato/core/gelato/components/create-task/resolver-input.tsx index 5efbce3..c8bac7e 100644 --- a/template/integrations/gelato/core/gelato/components/create-task/resolver-input.tsx +++ b/template/integrations/gelato/core/gelato/components/create-task/resolver-input.tsx @@ -1,25 +1,43 @@ -import { ContractInput } from './contract-input' -import { ExecutionValues } from './execution-values' -import { FunctionInput } from './function-input' -import { GELATO_CONSTANTS } from '../../utils/constants' +import { GELATO_CONSTANTS } from "../../utils/constants" +import { ContractInput } from "./contract-input" +import { ExecutionValues } from "./execution-values" +import { FunctionInput } from "./function-input" export function ResolverInput() { return ( -
    +

    When

    -
    Resolver Contracts allow you to define arbitrary conditions that should trigger an execution.
    +
    - - - + + +
    diff --git a/template/integrations/gelato/core/gelato/components/create-task/restriction-info.tsx b/template/integrations/gelato/core/gelato/components/create-task/restriction-info.tsx index dd3f3a6..5035a63 100644 --- a/template/integrations/gelato/core/gelato/components/create-task/restriction-info.tsx +++ b/template/integrations/gelato/core/gelato/components/create-task/restriction-info.tsx @@ -1,18 +1,21 @@ -import { useMemo, useState } from 'react' - -import CopyToClipboard from 'react-copy-to-clipboard' -import { useFormContext } from 'react-hook-form' -import { FaCopy } from 'react-icons/fa' - -import { CreateTaskForm } from './create-task' -import { getFunctionSignature, truncateEthAddress } from '../../utils/helpers' - -export function RestrictionInfo({ dedicatedMsgSender }: { dedicatedMsgSender?: string }) { +import { useMemo, useState } from "react" +import CopyToClipboard from "react-copy-to-clipboard" +import { useFormContext } from "react-hook-form" +import { FaCopy } from "react-icons/fa" + +import { getFunctionSignature, truncateEthAddress } from "../../utils/helpers" +import { CreateTaskForm } from "./create-task" + +export function RestrictionInfo({ + dedicatedMsgSender, +}: { + dedicatedMsgSender?: string +}) { const [copied, setCopied] = useState(false) const { watch } = useFormContext() - const [abi, func] = watch(['abi', 'func']) + const [abi, func] = watch(["abi", "func"]) const functionSignature = useMemo(() => { if (!abi || !func) return false @@ -21,23 +24,26 @@ export function RestrictionInfo({ dedicatedMsgSender }: { dedicatedMsgSender?: s }, [abi, func]) return ( -
    +
    - If the {functionSignature} function has access restrictions, whitelist this address as a{' '} + If the {functionSignature}{" "} + function has access restrictions, whitelist this address as a{" "} msg.sender
    { setCopied(true) setTimeout(() => setCopied(false), 3000) - }}> + }} + >
    {copied ? ( <>Copied! ) : ( <> - {truncateEthAddress(dedicatedMsgSender || '', 20)} + {truncateEthAddress(dedicatedMsgSender || "", 20)}{" "} + )}
    diff --git a/template/integrations/gelato/core/gelato/components/create-task/task-name-input.tsx b/template/integrations/gelato/core/gelato/components/create-task/task-name-input.tsx index 9e73bbc..44f02ef 100644 --- a/template/integrations/gelato/core/gelato/components/create-task/task-name-input.tsx +++ b/template/integrations/gelato/core/gelato/components/create-task/task-name-input.tsx @@ -1,17 +1,21 @@ -import { useFormContext } from 'react-hook-form' +import { useFormContext } from "react-hook-form" -import { CreateTaskForm } from './create-task' +import { CreateTaskForm } from "./create-task" export function TaskNameInput() { const { register } = useFormContext() return ( -
    +

    Task name

    - +
    ) diff --git a/template/integrations/gelato/core/gelato/components/index.tsx b/template/integrations/gelato/core/gelato/components/index.tsx index 5ed455d..bcbb2e9 100644 --- a/template/integrations/gelato/core/gelato/components/index.tsx +++ b/template/integrations/gelato/core/gelato/components/index.tsx @@ -1,4 +1,4 @@ -export * from './active-tasks' -export * from './create-task' -export * from './task-view' -export * from './rename-task' +export * from "./active-tasks" +export * from "./create-task" +export * from "./task-view" +export * from "./rename-task" diff --git a/template/integrations/gelato/core/gelato/components/rename-task.tsx b/template/integrations/gelato/core/gelato/components/rename-task.tsx index 2822711..0405070 100644 --- a/template/integrations/gelato/core/gelato/components/rename-task.tsx +++ b/template/integrations/gelato/core/gelato/components/rename-task.tsx @@ -1,19 +1,30 @@ -import { useForm } from 'react-hook-form' -import { FaSpinner } from 'react-icons/fa' +import { useForm } from "react-hook-form" +import { FaSpinner } from "react-icons/fa" -import { ValidationError } from './errors/validation-error' -import { useRenameTask } from '../hooks' +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" + +import { useRenameTask } from "../hooks" +import { ValidationError } from "./errors/validation-error" type RenameTaskForm = { name: string } -export function RenameTask({ taskId, name, onSave }: { taskId: string; name: string; onSave: () => void }) { +export function RenameTask({ + taskId, + name, + onSave, +}: { + taskId: string + name: string + onSave: () => void +}) { const form = useForm({ defaultValues: { name, }, - mode: 'all', + mode: "all", }) const { mutateAsync: renameTask, isLoading } = useRenameTask() @@ -21,7 +32,7 @@ export function RenameTask({ taskId, name, onSave }: { taskId: string; name: str const onSubmit = async () => { const { name } = form.getValues() - renameTask({ + await renameTask({ taskId, name, }) @@ -32,26 +43,36 @@ export function RenameTask({ taskId, name, onSave }: { taskId: string; name: str } return ( -
    -
    -
    -
    Rename task
    - (val.length > 3 ? true : 'Name needs to be min 3 characters long'), - }, - })} - className="input max-w-md !rounded-2xl dark:!bg-zinc-700 dark:!text-white" - defaultValue={name} - /> - -
    - - -
    + + +
    +
    +
    Rename task
    + + val.length > 3 + ? true + : "Name needs to be min 3 characters long", + }, + })} + className="input max-w-md !rounded-2xl dark:!bg-zinc-700 dark:!text-white" + defaultValue={name} + /> + +
    + + +
    +
    ) } diff --git a/template/integrations/gelato/core/gelato/components/task-view/executing-address.tsx b/template/integrations/gelato/core/gelato/components/task-view/executing-address.tsx index de69e3a..63a045d 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/executing-address.tsx +++ b/template/integrations/gelato/core/gelato/components/task-view/executing-address.tsx @@ -1,10 +1,9 @@ -import { useState } from 'react' +import { useState } from "react" +import CopyToClipboard from "react-copy-to-clipboard" +import { FaCopy } from "react-icons/fa" -import CopyToClipboard from 'react-copy-to-clipboard' -import { FaCopy } from 'react-icons/fa' - -import { useMsgSender } from '../../hooks/use-msg-sender' -import { truncateEthAddress } from '../../utils/helpers' +import { useMsgSender } from "../../hooks/use-msg-sender" +import { truncateEthAddress } from "../../utils/helpers" export function ExecutingAddress() { const [copied, setCopied] = useState(false) @@ -19,17 +18,21 @@ export function ExecutingAddress() {

    Your msg.sender

    { setCopied(true) setTimeout(() => setCopied(false), 3000) - }}> + }} + >

    {copied ? ( <>Copied! ) : ( <> - {truncateEthAddress(dedicatedMsgSender?.address || '', 20)} + + {truncateEthAddress(dedicatedMsgSender?.address || "", 20)} + {" "} + )}
    diff --git a/template/integrations/gelato/core/gelato/components/task-view/function-data.tsx b/template/integrations/gelato/core/gelato/components/task-view/function-data.tsx index 9eaa40a..5b2ecf4 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/function-data.tsx +++ b/template/integrations/gelato/core/gelato/components/task-view/function-data.tsx @@ -1,4 +1,4 @@ -import { getTaskFunctionData } from '../../utils/helpers' +import { getTaskFunctionData } from "../../utils/helpers" export type FunctionDataProps = { functionData: ReturnType @@ -8,7 +8,9 @@ export function FunctionData({ functionData }: FunctionDataProps) { return (
    -

    Automated Function

    +

    + Automated Function +

    @@ -17,12 +19,16 @@ export function FunctionData({ functionData }: FunctionDataProps) { ( {functionData.func.inputs.map((input, index) => ( - {input.type} + + {input.type} + : - + {input.name} - {index < functionData.func.inputs.length - 1 && ,} + {index < functionData.func.inputs.length - 1 && ( + , + )} ))} ) diff --git a/template/integrations/gelato/core/gelato/components/task-view/index.ts b/template/integrations/gelato/core/gelato/components/task-view/index.ts index 2725295..9f95995 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/index.ts +++ b/template/integrations/gelato/core/gelato/components/task-view/index.ts @@ -1 +1 @@ -export * from './task-view' +export * from "./task-view" diff --git a/template/integrations/gelato/core/gelato/components/task-view/input-values.tsx b/template/integrations/gelato/core/gelato/components/task-view/input-values.tsx index 176db67..8ed0ef6 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/input-values.tsx +++ b/template/integrations/gelato/core/gelato/components/task-view/input-values.tsx @@ -1,4 +1,4 @@ -import { getTaskFunctionData } from '../../utils/helpers' +import { getTaskFunctionData } from "../../utils/helpers" export type InputValuesProps = { functionData: ReturnType @@ -14,7 +14,9 @@ export function InputValues({ functionData }: InputValuesProps) { {item.type} {item.name}

    -

    {String(functionData.data[index])}

    +

    + {String(functionData.data[index])} +

    ))}
    diff --git a/template/integrations/gelato/core/gelato/components/task-view/interval-values.tsx b/template/integrations/gelato/core/gelato/components/task-view/interval-values.tsx index 67f6cb0..069d8a4 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/interval-values.tsx +++ b/template/integrations/gelato/core/gelato/components/task-view/interval-values.tsx @@ -1,4 +1,4 @@ -import moment from 'moment' +import moment from "moment" export type IntervalValuesProps = { startTime?: string | null @@ -6,15 +6,19 @@ export type IntervalValuesProps = { createdAt: string } -export function IntervalValues({ startTime, interval, createdAt }: IntervalValuesProps) { - const nextExecution = parseInt(startTime?.toString() || '') +export function IntervalValues({ + startTime, + interval, + createdAt, +}: IntervalValuesProps) { + const nextExecution = parseInt(startTime?.toString() || "") ? moment(startTime?.toString()) - .add(parseInt(interval as string), 'seconds') - .format('ll, HH:mm:ss') + .add(parseInt(interval as string), "seconds") + .format("ll, HH:mm:ss") : moment .unix(parseInt(createdAt)) - .add(parseInt(interval as string), 'seconds') - .format('ll, HH:mm:ss') + .add(parseInt(interval as string), "seconds") + .format("ll, HH:mm:ss") return (
    @@ -25,17 +29,25 @@ export function IntervalValues({ startTime, interval, createdAt }: IntervalValue <>

    Interval

    -

    {moment.duration({ seconds: parseInt(interval || '0') }).humanize()}

    +

    + {moment + .duration({ seconds: parseInt(interval || "0") }) + .humanize()} +

    -

    Next Execution

    +

    + Next Execution +

    {nextExecution}

    ) : (

    Next execution

    -

    Will be attempted at the next block

    +

    + Will be attempted at the next block +

    )}
    diff --git a/template/integrations/gelato/core/gelato/components/task-view/payment-info.tsx b/template/integrations/gelato/core/gelato/components/task-view/payment-info.tsx index 5ddccda..a087a5d 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/payment-info.tsx +++ b/template/integrations/gelato/core/gelato/components/task-view/payment-info.tsx @@ -1,4 +1,8 @@ -export function PaymentInfo({ useTaskTreasuryFunds }: { useTaskTreasuryFunds: boolean }) { +export function PaymentInfo({ + useTaskTreasuryFunds, +}: { + useTaskTreasuryFunds: boolean +}) { return (
    @@ -6,7 +10,9 @@ export function PaymentInfo({ useTaskTreasuryFunds }: { useTaskTreasuryFunds: bo

    Spend

    -

    {useTaskTreasuryFunds ? 'Gelato Balance' : 'Contract Funds'}

    +

    + {useTaskTreasuryFunds ? "Gelato Balance" : "Contract Funds"} +

    ) diff --git a/template/integrations/gelato/core/gelato/components/task-view/resolver-values.tsx b/template/integrations/gelato/core/gelato/components/task-view/resolver-values.tsx index a1fe9cf..185a15b 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/resolver-values.tsx +++ b/template/integrations/gelato/core/gelato/components/task-view/resolver-values.tsx @@ -1,12 +1,15 @@ -import { InputValues } from './input-values' -import { getTaskFunctionData } from '../../utils/helpers' +import { getTaskFunctionData } from "../../utils/helpers" +import { InputValues } from "./input-values" export type ResolverValuesProps = { functionData: ReturnType resolverAddress: string } -export function ResolverValues({ functionData, resolverAddress }: ResolverValuesProps) { +export function ResolverValues({ + functionData, + resolverAddress, +}: ResolverValuesProps) { return (
    @@ -28,10 +31,12 @@ export function ResolverValues({ functionData, resolverAddress }: ResolverValues {input.type} : - + {input.name} - {index < functionData.func.inputs.length - 1 && ,} + {index < functionData.func.inputs.length - 1 && ( + , + )} ))} ) diff --git a/template/integrations/gelato/core/gelato/components/task-view/task-view.tsx b/template/integrations/gelato/core/gelato/components/task-view/task-view.tsx index 733099e..67c0987 100644 --- a/template/integrations/gelato/core/gelato/components/task-view/task-view.tsx +++ b/template/integrations/gelato/core/gelato/components/task-view/task-view.tsx @@ -1,22 +1,26 @@ -import { useMemo, useState } from 'react' - -import moment from 'moment' -import Link from 'next/link' -import { FaEdit, FaExternalLinkAlt, FaTimesCircle } from 'react-icons/fa' -import { FiChevronLeft } from 'react-icons/fi' -import { useNetwork } from 'wagmi' - -import { ExecutingAddress } from './executing-address' -import { FunctionData } from './function-data' -import { InputValues } from './input-values' -import { IntervalValues } from './interval-values' -import { PaymentInfo } from './payment-info' -import { ResolverValues } from './resolver-values' -import { useAbi, useTask } from '../../hooks' -import { useTaskResolver } from '../../hooks/use-task-resolver' -import { formatFee, getAddressUrl, getTaskFunctionData, truncateEthAddress } from '../../utils/helpers' -import { decodeModuleArgs } from '../../utils/resolverDecoder' -import { RenameTask } from '../rename-task' +import { useMemo, useState } from "react" +import Link from "next/link" +import moment from "moment" +import { FaEdit, FaExternalLinkAlt, FaTimesCircle } from "react-icons/fa" +import { FiChevronLeft } from "react-icons/fi" +import { useNetwork } from "wagmi" + +import { useAbi, useTask } from "../../hooks" +import { useTaskResolver } from "../../hooks/use-task-resolver" +import { + formatFee, + getAddressUrl, + getTaskFunctionData, + truncateEthAddress, +} from "../../utils/helpers" +import { decodeModuleArgs } from "../../utils/resolverDecoder" +import { RenameTask } from "../rename-task" +import { ExecutingAddress } from "./executing-address" +import { FunctionData } from "./function-data" +import { InputValues } from "./input-values" +import { IntervalValues } from "./interval-values" +import { PaymentInfo } from "./payment-info" +import { ResolverValues } from "./resolver-values" export type TasKViewProps = { taskId: string @@ -28,23 +32,34 @@ export function TaskView({ taskId }: TasKViewProps) { const { data: taskWithName, isLoading, refetch } = useTask({ taskId }) const { chain } = useNetwork() - const { data: abi } = useAbi({ contractAddress: taskWithName?.task.execAddress as string }) + const { data: abi } = useAbi({ + contractAddress: taskWithName?.task.execAddress as string, + }) const { data: taskResolver } = useTaskResolver({ taskId }) - const { data: resolverAbi } = useAbi({ contractAddress: taskResolver?.address as string }) + const { data: resolverAbi } = useAbi({ + contractAddress: taskResolver?.address as string, + }) const functionData = useMemo(() => { if (!taskWithName) return const { moduleArgs, modules } = taskWithName.task - const decodedArgs = decodeModuleArgs(moduleArgs as string[], modules as number[]) + const decodedArgs = decodeModuleArgs( + moduleArgs as string[], + modules as number[] + ) if (taskResolver && resolverAbi && taskResolver.address) { return { args: decodedArgs, - data: getTaskFunctionData(taskResolver.address, resolverAbi, decodedArgs.resolverData as string), + data: getTaskFunctionData( + taskResolver.address, + resolverAbi, + decodedArgs.resolverData as string + ), } } @@ -52,7 +67,11 @@ export function TaskView({ taskId }: TasKViewProps) { return { args: decodedArgs, - data: getTaskFunctionData(taskWithName.task.execAddress, abi, taskWithName.task.execDataOrSelector as string), + data: getTaskFunctionData( + taskWithName.task.execAddress, + abi, + taskWithName.task.execDataOrSelector as string + ), } }, [abi, taskWithName, taskResolver, resolverAbi]) @@ -62,7 +81,9 @@ export function TaskView({ taskId }: TasKViewProps) { } if (isLoading || !taskWithName) { - return
    + return ( +
    + ) } const { task, name } = taskWithName @@ -71,41 +92,70 @@ export function TaskView({ taskId }: TasKViewProps) {
    - + Back
    -

    {truncateEthAddress(name, 30)}

    +

    + {truncateEthAddress(name, 30)} +

    {showRename ? ( - setShowRename(false)} /> + setShowRename(false)} + /> ) : ( - setShowRename(true)} /> + setShowRename(true)} + /> )}
    - •  + + {" "} + • {" "} + {task.status}
    - {showRename && handleRename()} />} + {showRename && ( + handleRename()} + /> + )}
    Created By: + target="_blank" + > {truncateEthAddress(task.taskCreator.id, 20)} - {moment.unix(task.createdAt as number).format('ll, HH:mm:ss')} + + {moment.unix(task.createdAt as number).format("ll, HH:mm:ss")} +
    Task ID: {taskId} @@ -116,25 +166,36 @@ export function TaskView({ taskId }: TasKViewProps) {
    Executions
    -
    $ {task.feeTotalUsd ? formatFee(task.feeTotalUsd as string) : '0.00'}
    +
    + ${" "} + {task.feeTotalUsd + ? formatFee(task.feeTotalUsd as string) + : "0.00"} +
    Cost
    -
    +

    Execute

    -

    Target Contract

    +

    + Target Contract +

    {!taskResolver?.address ? ( <> - {functionData && } -
    - {functionData && } -
    - + {functionData && ( + + )} +
    + {functionData && ( + + )} +
    + ) : ( <> -
    - {functionData && } +
    + {functionData && ( + + )} )} -
    +
    -
    +
    diff --git a/template/integrations/gelato/core/gelato/graphql/codegen.ts b/template/integrations/gelato/core/gelato/graphql/codegen.ts index 0cbc7b1..dfdfe43 100644 --- a/template/integrations/gelato/core/gelato/graphql/codegen.ts +++ b/template/integrations/gelato/core/gelato/graphql/codegen.ts @@ -1,17 +1,16 @@ -import { join } from 'path' - -import { CodegenConfig } from '@graphql-codegen/cli' +import { join } from "path" +import { CodegenConfig } from "@graphql-codegen/cli" const pth = (path: string) => { return join(__dirname, path) } const config: CodegenConfig = { - schema: 'https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me', - documents: [pth('/tasks.graphql')], + schema: "https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me", + documents: [pth("/tasks.graphql")], generates: { - [pth('graphql/generated/')]: { - preset: 'client', + [pth("graphql/generated/")]: { + preset: "client", presetConfig: { fragmentMasking: false, }, diff --git a/template/integrations/gelato/core/gelato/graphql/tasks.graphql b/template/integrations/gelato/core/gelato/graphql/tasks.graphql index 4e33384..b3369a0 100644 --- a/template/integrations/gelato/core/gelato/graphql/tasks.graphql +++ b/template/integrations/gelato/core/gelato/graphql/tasks.graphql @@ -1,5 +1,11 @@ query getAllTaskData($taskCreator: String, $limit: Int, $skip: Int) { - tasks(first: $limit, skip: $skip, where: { taskCreator: $taskCreator }, orderBy: createdAt, orderDirection: desc) { + tasks( + first: $limit + skip: $skip + where: { taskCreator: $taskCreator } + orderBy: createdAt + orderDirection: desc + ) { id version { id diff --git a/template/integrations/gelato/core/gelato/hooks/index.ts b/template/integrations/gelato/core/gelato/hooks/index.ts index f3cb805..68f52ad 100644 --- a/template/integrations/gelato/core/gelato/hooks/index.ts +++ b/template/integrations/gelato/core/gelato/hooks/index.ts @@ -1,8 +1,8 @@ -export * from './use-active-tasks' -export * from './use-task' -export * from './use-abi' -export * from './use-new-task' -export * from './use-cancel-task' -export * from './use-rename-task' -export * from './use-is-automate-supported' -export * from './use-automate-sdk' +export * from "./use-active-tasks" +export * from "./use-task" +export * from "./use-abi" +export * from "./use-new-task" +export * from "./use-cancel-task" +export * from "./use-rename-task" +export * from "./use-is-automate-supported" +export * from "./use-automate-sdk" diff --git a/template/integrations/gelato/core/gelato/hooks/use-abi.ts b/template/integrations/gelato/core/gelato/hooks/use-abi.ts index 45ecf78..bb2c862 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-abi.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-abi.ts @@ -1,18 +1,24 @@ -import { useQuery } from '@tanstack/react-query' -import axios from 'axios' -import { isAddress } from 'viem' -import { useNetwork } from 'wagmi' +import { useQuery } from "@tanstack/react-query" +import axios from "axios" +import { isAddress } from "viem" +import { useNetwork } from "wagmi" -import { GELATO_CONSTANTS } from '../utils/constants' -import { ExplorerFetchAbiResponse } from '../utils/types' +import { GELATO_CONSTANTS } from "../utils/constants" +import { ExplorerFetchAbiResponse } from "../utils/types" -const abiFetcher = async ({ contractAddress, chainId }: { contractAddress: string; chainId: number }) => { +const abiFetcher = async ({ + contractAddress, + chainId, +}: { + contractAddress: string + chainId: number +}) => { const { explorerApiUrl, explorerApiKey } = GELATO_CONSTANTS.networks[chainId] const response = await axios.get(explorerApiUrl, { params: { - module: 'contract', - action: 'getsourcecode', + module: "contract", + action: "getsourcecode", address: contractAddress, apikey: explorerApiKey, }, @@ -24,10 +30,10 @@ const abiFetcher = async ({ contractAddress, chainId }: { contractAddress: strin export const useAbi = ({ contractAddress }: { contractAddress: string }) => { const { chain } = useNetwork() - return useQuery(['gelato-contract-abi', chain?.id, contractAddress], { + return useQuery(["gelato-contract-abi", chain?.id, contractAddress], { queryFn: () => { if (!chain?.id || !contractAddress || !isAddress(contractAddress)) { - throw new Error('Invalid Parameters') + throw new Error("Invalid Parameters") } return abiFetcher({ contractAddress, chainId: chain.id }) diff --git a/template/integrations/gelato/core/gelato/hooks/use-active-tasks.ts b/template/integrations/gelato/core/gelato/hooks/use-active-tasks.ts index 1abe621..4a4e136 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-active-tasks.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-active-tasks.ts @@ -1,16 +1,22 @@ -import { useQuery } from '@tanstack/react-query' -import { request } from 'graphql-request' -import { useAccount, useNetwork } from 'wagmi' - -import { useGelatoAutomateSdk } from './use-automate-sdk' -import { GetAllTaskDataDocument, GetAllTaskDataQuery, GetAllTaskDataQueryVariables } from '../graphql/graphql/generated/graphql' -import { GELATO_CONSTANTS } from '../utils/constants' -import { getGqlEndpoint } from '../utils/helpers' -import { FetchActiveTasksProps } from '../utils/types' +import { useQuery } from "@tanstack/react-query" +import { request } from "graphql-request" +import { useAccount, useNetwork } from "wagmi" + +import { + GetAllTaskDataDocument, + GetAllTaskDataQuery, + GetAllTaskDataQueryVariables, +} from "../graphql/graphql/generated/graphql" +import { GELATO_CONSTANTS } from "../utils/constants" +import { getGqlEndpoint } from "../utils/helpers" +import { FetchActiveTasksProps } from "../utils/types" +import { useGelatoAutomateSdk } from "./use-automate-sdk" const fetchActiveTasks = ({ address, gqlEndpoint }: FetchActiveTasksProps) => { return request({ - url: `https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me` + gqlEndpoint, + url: + `https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me` + + gqlEndpoint, document: GetAllTaskDataDocument, variables: { taskCreator: address?.toLowerCase(), @@ -27,14 +33,17 @@ export const useActiveTasks = () => { const chainId = chain?.id - return useQuery(['gelato-tasks', address, automateSdk], { + return useQuery(["gelato-tasks", address, automateSdk], { queryFn: async () => { - if (!chainId || !GELATO_CONSTANTS.networks[chainId] || !address) throw new Error('Missing Parameters') + if (!chainId || !GELATO_CONSTANTS.networks[chainId] || !address) + throw new Error("Missing Parameters") const gqlEndpoint = getGqlEndpoint(chain.id) const tasks = await fetchActiveTasks({ address, gqlEndpoint }) - const names = await automateSdk?.getTaskNames(tasks.tasks.map((task) => task.id)) + const names = await automateSdk?.getTaskNames( + tasks.tasks.map((task) => task.id) + ) return { tasks: tasks.tasks, names } }, diff --git a/template/integrations/gelato/core/gelato/hooks/use-automate-sdk.ts b/template/integrations/gelato/core/gelato/hooks/use-automate-sdk.ts index 148febe..553d05d 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-automate-sdk.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-automate-sdk.ts @@ -1,9 +1,8 @@ -import { useEffect, useState } from 'react' +import { useEffect, useState } from "react" +import { AutomateSDK } from "@gelatonetwork/automate-sdk" +import { useNetwork } from "wagmi" -import { AutomateSDK } from '@gelatonetwork/automate-sdk' -import { useNetwork } from 'wagmi' - -import { useEthersSigner } from '@/lib/hooks/web3/use-ethers-signer' +import { useEthersSigner } from "@/lib/hooks/web3/use-ethers-signer" export const useGelatoAutomateSdk = () => { const [automateSdk, setAutomateSdk] = useState() diff --git a/template/integrations/gelato/core/gelato/hooks/use-cancel-task.ts b/template/integrations/gelato/core/gelato/hooks/use-cancel-task.ts index b20dbb1..964670b 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-cancel-task.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-cancel-task.ts @@ -1,7 +1,7 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation } from "@tanstack/react-query" -import { useGelatoAutomateSdk } from './use-automate-sdk' -import { UseCancelTaskProps } from '../utils/types' +import { UseCancelTaskProps } from "../utils/types" +import { useGelatoAutomateSdk } from "./use-automate-sdk" export const useCancelTask = () => { const { automateSdk } = useGelatoAutomateSdk() diff --git a/template/integrations/gelato/core/gelato/hooks/use-is-automate-supported.ts b/template/integrations/gelato/core/gelato/hooks/use-is-automate-supported.ts index 70ae1bb..883bf00 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-is-automate-supported.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-is-automate-supported.ts @@ -1,5 +1,5 @@ -import { isAutomateSupported } from '@gelatonetwork/automate-sdk' -import { useNetwork } from 'wagmi' +import { isAutomateSupported } from "@gelatonetwork/automate-sdk" +import { useNetwork } from "wagmi" export const useIsAutomateSupported = () => { const { chain } = useNetwork() diff --git a/template/integrations/gelato/core/gelato/hooks/use-msg-sender.ts b/template/integrations/gelato/core/gelato/hooks/use-msg-sender.ts index 984ac9c..91fb096 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-msg-sender.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-msg-sender.ts @@ -1,11 +1,11 @@ -import { useQuery } from '@tanstack/react-query' +import { useQuery } from "@tanstack/react-query" -import { useGelatoAutomateSdk } from './use-automate-sdk' +import { useGelatoAutomateSdk } from "./use-automate-sdk" export const useMsgSender = () => { const { automateSdk } = useGelatoAutomateSdk() - return useQuery(['msg-sender', automateSdk], { + return useQuery(["msg-sender", automateSdk], { queryFn: () => { return automateSdk?.getDedicatedMsgSender() }, diff --git a/template/integrations/gelato/core/gelato/hooks/use-new-task.ts b/template/integrations/gelato/core/gelato/hooks/use-new-task.ts index 0a8793d..dce608b 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-new-task.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-new-task.ts @@ -1,14 +1,14 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation } from "@tanstack/react-query" -import { useGelatoAutomateSdk } from './use-automate-sdk' -import { UseNewTaskProps } from '../utils/types' +import { UseNewTaskProps } from "../utils/types" +import { useGelatoAutomateSdk } from "./use-automate-sdk" export const useNewTask = () => { const { automateSdk } = useGelatoAutomateSdk() return useMutation({ mutationFn: async ({ args, overrides, authToken }: UseNewTaskProps) => { - if (!automateSdk) throw new Error('Sdk not initiated') + if (!automateSdk) throw new Error("Sdk not initiated") return automateSdk.createTask(args, overrides, authToken) }, diff --git a/template/integrations/gelato/core/gelato/hooks/use-rename-task.ts b/template/integrations/gelato/core/gelato/hooks/use-rename-task.ts index 9fa56c3..5e5ed10 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-rename-task.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-rename-task.ts @@ -1,7 +1,7 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation } from "@tanstack/react-query" -import { useGelatoAutomateSdk } from './use-automate-sdk' -import { UseRenameTaskProps } from '../utils/types' +import { UseRenameTaskProps } from "../utils/types" +import { useGelatoAutomateSdk } from "./use-automate-sdk" export const useRenameTask = () => { const { automateSdk } = useGelatoAutomateSdk() diff --git a/template/integrations/gelato/core/gelato/hooks/use-task-resolver.ts b/template/integrations/gelato/core/gelato/hooks/use-task-resolver.ts index 228bcd9..7f484ff 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-task-resolver.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-task-resolver.ts @@ -1,16 +1,18 @@ -import { useQuery } from '@tanstack/react-query' -import axios from 'axios' -import { useNetwork } from 'wagmi' +import { useQuery } from "@tanstack/react-query" +import axios from "axios" +import { useNetwork } from "wagmi" -import { FetchResolverResponse } from '../utils/types' +import { FetchResolverResponse } from "../utils/types" export const useTaskResolver = ({ taskId }: { taskId: string }) => { const { chain } = useNetwork() - return useQuery(['gelato-task-resolver', taskId, chain?.id], { + return useQuery(["gelato-task-resolver", taskId, chain?.id], { queryFn: async () => { const response = await axios.get( - `https://ops-task.fra.gelato.digital/1514007e8336fa99e6fe/api/contracts/${chain?.id.toString() as string}/?taskId=${taskId}&resolver=true` + `https://ops-task.fra.gelato.digital/1514007e8336fa99e6fe/api/contracts/${ + chain?.id.toString() as string + }/?taskId=${taskId}&resolver=true` ) return response.data }, diff --git a/template/integrations/gelato/core/gelato/hooks/use-task.ts b/template/integrations/gelato/core/gelato/hooks/use-task.ts index 6309261..4288643 100644 --- a/template/integrations/gelato/core/gelato/hooks/use-task.ts +++ b/template/integrations/gelato/core/gelato/hooks/use-task.ts @@ -1,15 +1,21 @@ -import { useQuery } from '@tanstack/react-query' -import request from 'graphql-request' -import { useNetwork } from 'wagmi' +import { useQuery } from "@tanstack/react-query" +import request from "graphql-request" +import { useNetwork } from "wagmi" -import { useGelatoAutomateSdk } from './use-automate-sdk' -import { GetTaskDocument, GetTaskQuery, GetTaskQueryVariables } from '../graphql/graphql/generated/graphql' -import { getGqlEndpoint } from '../utils/helpers' -import { FetchTaskProps } from '../utils/types' +import { + GetTaskDocument, + GetTaskQuery, + GetTaskQueryVariables, +} from "../graphql/graphql/generated/graphql" +import { getGqlEndpoint } from "../utils/helpers" +import { FetchTaskProps } from "../utils/types" +import { useGelatoAutomateSdk } from "./use-automate-sdk" const fetchTask = ({ id, gqlEndpoint }: FetchTaskProps) => { return request({ - url: `https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me` + gqlEndpoint, + url: + `https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me` + + gqlEndpoint, document: GetTaskDocument, variables: { id, @@ -21,12 +27,15 @@ export const useTask = ({ taskId }: { taskId: string }) => { const { automateSdk } = useGelatoAutomateSdk() const { chain } = useNetwork() - return useQuery(['gelato-task', taskId, automateSdk], { + return useQuery(["gelato-task", taskId, automateSdk], { queryFn: async () => { const taskNames = await automateSdk?.getTaskNames([taskId]) - const task = await fetchTask({ id: taskId, gqlEndpoint: getGqlEndpoint(chain?.id as number) }) + const task = await fetchTask({ + id: taskId, + gqlEndpoint: getGqlEndpoint(chain?.id as number), + }) - if (!taskNames?.[0] || !task.task) throw new Error('Error fetching task') + if (!taskNames?.[0] || !task.task) throw new Error("Error fetching task") return { task: task.task, diff --git a/template/integrations/gelato/core/gelato/index.ts b/template/integrations/gelato/core/gelato/index.ts index 7aea380..f18f992 100644 --- a/template/integrations/gelato/core/gelato/index.ts +++ b/template/integrations/gelato/core/gelato/index.ts @@ -1,2 +1,2 @@ -export * from './components' -export * from './hooks' +export * from "./components" +export * from "./hooks" diff --git a/template/integrations/gelato/core/gelato/utils/constants.ts b/template/integrations/gelato/core/gelato/utils/constants.ts index 0083674..c9ba748 100644 --- a/template/integrations/gelato/core/gelato/utils/constants.ts +++ b/template/integrations/gelato/core/gelato/utils/constants.ts @@ -1,111 +1,122 @@ -import { GelatoConstants } from './types' +import { GelatoConstants } from "./types" export const GELATO_CONSTANTS: GelatoConstants = { - subgraphBaseUrl: 'https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me', + subgraphBaseUrl: + "https://api.thegraph.com/subgraphs/name/gelatodigital/poke-me", docs: { - payment: 'https://docs.gelato.network/developer-services/automate/paying-for-your-transactions', - resolver: 'https://docs.gelato.network/developer-services/automate/guides/custom-logic-triggers', + payment: + "https://docs.gelato.network/developer-services/automate/paying-for-your-transactions", + resolver: + "https://docs.gelato.network/developer-services/automate/guides/custom-logic-triggers", }, networks: { 1: { - graph: '', - contract: '0x25aD59adbe00C2d80c86d01e2E05e1294DA84823', - explorerApiUrl: 'https://api.etherscan.io/api', - explorerUrl: 'https://etherscan.io', + graph: "", + contract: "0x25aD59adbe00C2d80c86d01e2E05e1294DA84823", + explorerApiUrl: "https://api.etherscan.io/api", + explorerUrl: "https://etherscan.io", explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_MAINNET as string, }, 137: { - graph: 'polygon', - contract: '0x527a819db1eb0e34426297b03bae11F2f8B3A19E', - explorerApiUrl: 'https://api.polygonscan.com/api', - explorerUrl: 'https://polygonscan.com', + graph: "polygon", + contract: "0x527a819db1eb0e34426297b03bae11F2f8B3A19E", + explorerApiUrl: "https://api.polygonscan.com/api", + explorerUrl: "https://polygonscan.com", explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_POLYGON as string, }, 250: { - graph: 'fantom', - contract: '0x6EDe1597c05A0ca77031cBA43Ab887ccf24cd7e8', - explorerApiUrl: 'https://api.ftmscan.com', - explorerUrl: 'https://ftmscan.com', + graph: "fantom", + contract: "0x6EDe1597c05A0ca77031cBA43Ab887ccf24cd7e8", + explorerApiUrl: "https://api.ftmscan.com", + explorerUrl: "https://ftmscan.com", explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_FANTOM as string, }, 42161: { - graph: 'arbitrum', - contract: '0xB3f5503f93d5Ef84b06993a1975B9D21B962892F', - explorerApiUrl: 'https://api.arbiscan.io/api', - explorerUrl: 'https://arbiscan.io', - explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_ARBITRUM as string, + graph: "arbitrum", + contract: "0xB3f5503f93d5Ef84b06993a1975B9D21B962892F", + explorerApiUrl: "https://api.arbiscan.io/api", + explorerUrl: "https://arbiscan.io", + explorerApiKey: process.env + .NEXT_PUBLIC_GELATO_SCAN_KEY_ARBITRUM as string, }, 43114: { - graph: 'avalanche', - contract: '0x8aB6aDbC1fec4F18617C9B889F5cE7F28401B8dB', - explorerApiUrl: 'https://api.avascan.info/api', - explorerUrl: 'https://avascan.info', - explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_AVALANCHE as string, + graph: "avalanche", + contract: "0x8aB6aDbC1fec4F18617C9B889F5cE7F28401B8dB", + explorerApiUrl: "https://api.avascan.info/api", + explorerUrl: "https://avascan.info", + explorerApiKey: process.env + .NEXT_PUBLIC_GELATO_SCAN_KEY_AVALANCHE as string, }, 56: { - graph: 'bsc', - contract: '0x527a819db1eb0e34426297b03bae11F2f8B3A19E', - explorerApiUrl: 'https://api.bscscan.com/api', - explorerUrl: 'https://bscscan.com/', + graph: "bsc", + contract: "0x527a819db1eb0e34426297b03bae11F2f8B3A19E", + explorerApiUrl: "https://api.bscscan.com/api", + explorerUrl: "https://bscscan.com/", explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_BSC as string, }, 25: { - graph: 'cronos', - contract: '0x86B7e611194978F556007ac1F52D09d114D8f160', - explorerApiUrl: 'https://api.cronoscan.com/api', - explorerUrl: 'https://cronoscan.com', + graph: "cronos", + contract: "0x86B7e611194978F556007ac1F52D09d114D8f160", + explorerApiUrl: "https://api.cronoscan.com/api", + explorerUrl: "https://cronoscan.com", explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_CRONOS as string, }, 100: { - graph: 'gnosis-chain', - contract: '0x8aB6aDbC1fec4F18617C9B889F5cE7F28401B8dB', - explorerApiUrl: 'https://api.gnosisscan.io/api', - explorerUrl: 'https://gnosisscan.io', + graph: "gnosis-chain", + contract: "0x8aB6aDbC1fec4F18617C9B889F5cE7F28401B8dB", + explorerApiUrl: "https://api.gnosisscan.io/api", + explorerUrl: "https://gnosisscan.io", explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_GNOSIS as string, }, 10: { - graph: 'optimism', - contract: '0x340759c8346A1E6Ed92035FB8B6ec57cE1D82c2c', - explorerApiUrl: 'https://api-optimistic.etherscan.io/api', - explorerUrl: 'https://optimistic.etherscan.io/', - explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_OPTIMISM as string, + graph: "optimism", + contract: "0x340759c8346A1E6Ed92035FB8B6ec57cE1D82c2c", + explorerApiUrl: "https://api-optimistic.etherscan.io/api", + explorerUrl: "https://optimistic.etherscan.io/", + explorerApiKey: process.env + .NEXT_PUBLIC_GELATO_SCAN_KEY_OPTIMISM as string, }, 1284: { - graph: 'moonbeam', - contract: '0x6c3224f9b3feE000A444681d5D45e4532D5BA531', - explorerApiUrl: 'https://api-moonbeam.moonscan.io/api', - explorerUrl: 'https://moonscan.io', - explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_MOONBEAM as string, + graph: "moonbeam", + contract: "0x6c3224f9b3feE000A444681d5D45e4532D5BA531", + explorerApiUrl: "https://api-moonbeam.moonscan.io/api", + explorerUrl: "https://moonscan.io", + explorerApiKey: process.env + .NEXT_PUBLIC_GELATO_SCAN_KEY_MOONBEAM as string, }, 1285: { - graph: 'moonriver', - contract: '0x86B7e611194978F556007ac1F52D09d114D8f160', - explorerApiUrl: 'https://api-moonriver.moonscan.io/api', - explorerUrl: 'https://moonriver.moonscan.io', - explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_MOONRIVER as string, + graph: "moonriver", + contract: "0x86B7e611194978F556007ac1F52D09d114D8f160", + explorerApiUrl: "https://api-moonriver.moonscan.io/api", + explorerUrl: "https://moonriver.moonscan.io", + explorerApiKey: process.env + .NEXT_PUBLIC_GELATO_SCAN_KEY_MOONRIVER as string, }, 80001: { testnet: true, - graph: 'mumbai', - contract: '0xc1C6805B857Bef1f412519C4A842522431aFed39', - explorerApiUrl: 'https://api-mumbai.polygonscan.com/api', - explorerUrl: 'https://mumbai.polygonscan.com', - explorerApiKey: (process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_MUMBAI as string) || 'TTE1XRYPE4MRFH2I18BPRSB172PRNBRPBS', + graph: "mumbai", + contract: "0xc1C6805B857Bef1f412519C4A842522431aFed39", + explorerApiUrl: "https://api-mumbai.polygonscan.com/api", + explorerUrl: "https://mumbai.polygonscan.com", + explorerApiKey: + (process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_MUMBAI as string) || + "TTE1XRYPE4MRFH2I18BPRSB172PRNBRPBS", }, 421613: { testnet: true, - graph: 'arbitrum-goerli', - contract: '0xa5f9b728ecEB9A1F6FCC89dcc2eFd810bA4Dec41', - explorerApiUrl: 'https://api-goerli.arbiscan.io/api', - explorerUrl: 'https://goerli.arbiscan.io/', - explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_ARBITRUM_GOERLI as string, + graph: "arbitrum-goerli", + contract: "0xa5f9b728ecEB9A1F6FCC89dcc2eFd810bA4Dec41", + explorerApiUrl: "https://api-goerli.arbiscan.io/api", + explorerUrl: "https://goerli.arbiscan.io/", + explorerApiKey: process.env + .NEXT_PUBLIC_GELATO_SCAN_KEY_ARBITRUM_GOERLI as string, }, 5: { testnet: true, - graph: 'goerli', - contract: '0xc1C6805B857Bef1f412519C4A842522431aFed39', - explorerApiUrl: 'https://goerli.etherscan.io/api', - explorerUrl: 'https://goerli.etherscan.io', + graph: "goerli", + contract: "0xc1C6805B857Bef1f412519C4A842522431aFed39", + explorerApiUrl: "https://goerli.etherscan.io/api", + explorerUrl: "https://goerli.etherscan.io", explorerApiKey: process.env.NEXT_PUBLIC_GELATO_SCAN_KEY_GOERLI as string, }, }, diff --git a/template/integrations/gelato/core/gelato/utils/helpers.ts b/template/integrations/gelato/core/gelato/utils/helpers.ts index 9e98704..f82a541 100644 --- a/template/integrations/gelato/core/gelato/utils/helpers.ts +++ b/template/integrations/gelato/core/gelato/utils/helpers.ts @@ -1,7 +1,7 @@ -import { Abi, AbiFunction } from 'abitype' -import { ethers } from 'ethers' +import { Abi, AbiFunction } from "abitype" +import { ethers } from "ethers" -import { GELATO_CONSTANTS } from './constants' +import { GELATO_CONSTANTS } from "./constants" export const truncateEthAddress = (address: string, len?: number) => { len = len || 10 @@ -15,7 +15,7 @@ export const formatFee = (fee: string) => { } export const strLimit = (text: string, count: number) => { - return text.slice(0, count) + (text.length > count ? '...' : '') + return text.slice(0, count) + (text.length > count ? "..." : "") } export const isValidAbi = (abi: string) => { @@ -28,7 +28,11 @@ export const isValidAbi = (abi: string) => { } export const getAbiFunctions = (abi: Abi) => { - return abi.filter((item) => item.type === 'function' && ['payable', 'nonpayable'].indexOf(item.stateMutability) !== -1) as AbiFunction[] + return abi.filter( + (item) => + item.type === "function" && + ["payable", "nonpayable"].indexOf(item.stateMutability) !== -1 + ) as AbiFunction[] } export const isJsonArray = (str: string) => { @@ -40,58 +44,87 @@ export const isJsonArray = (str: string) => { } } -export const validateInput = (name: string, value: string, selectedFunctionAbi: AbiFunction) => { - if (value === '') return true +export const validateInput = ( + name: string, + value: string, + selectedFunctionAbi: AbiFunction +) => { + if (value === "") return true - const inputType = selectedFunctionAbi?.inputs.find((item) => item.name === name)?.type - if (!inputType) return 'Invalid Input Type' + const inputType = selectedFunctionAbi?.inputs.find( + (item) => item.name === name + )?.type + if (!inputType) return "Invalid Input Type" - const isArray = inputType.includes('[]') - const isNumeric = inputType.startsWith('uint') || inputType.startsWith('int') - const isAddress = inputType.startsWith('address') + const isArray = inputType.includes("[]") + const isNumeric = inputType.startsWith("uint") || inputType.startsWith("int") + const isAddress = inputType.startsWith("address") const errorMessages = { - array: 'Invalid array - make sure your array is in a correct format, e.g. ["1", "2", "3"].', - address: 'Invalid address - make sure argument is a correct Ethereum style address, e.g. "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2".', - number: 'Invalid number - make sure your integer variable is correct.', + array: + 'Invalid array - make sure your array is in a correct format, e.g. ["1", "2", "3"].', + address: + 'Invalid address - make sure argument is a correct Ethereum style address, e.g. "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2".', + number: "Invalid number - make sure your integer variable is correct.", } if (isArray) { if (!isJsonArray(value)) return errorMessages.array if (isAddress) { - const isAddressArray = (JSON.parse(value) as string[]).reduce((acc, value) => acc && ethers.utils.isAddress(value), true) + const isAddressArray = (JSON.parse(value) as string[]).reduce( + (acc, value) => acc && ethers.utils.isAddress(value), + true + ) return isAddressArray || errorMessages.address } if (isNumeric) { - const isNumericArray = (JSON.parse(value) as string[]).reduce((acc, value) => acc && parseInt(value) == (value as unknown), true) + const isNumericArray = (JSON.parse(value) as string[]).reduce( + (acc, value) => acc && parseInt(value) == (value as unknown), + true + ) return isNumericArray || errorMessages.number } } if (isAddress) return ethers.utils.isAddress(value) || errorMessages.address - if (isNumeric) return parseInt(value) == (value as unknown) || errorMessages.number + if (isNumeric) + return parseInt(value) == (value as unknown) || errorMessages.number return true } -export const inputsAreFilled = (selectedFunctionAbi: AbiFunction, inputs: { [key: string]: string }) => { - const functionInputs = selectedFunctionAbi.inputs.map((item) => item.name as string) +export const inputsAreFilled = ( + selectedFunctionAbi: AbiFunction, + inputs: { [key: string]: string } +) => { + const functionInputs = selectedFunctionAbi.inputs.map( + (item) => item.name as string + ) return functionInputs.reduce((acc, item) => acc && !!inputs[item], true) } export const getFunctionSignature = (abi: string, func: string) => { - const abiFunction = getAbiFunctions(JSON.parse(abi) as Abi).find((item) => item.name === func) as AbiFunction + const abiFunction = getAbiFunctions(JSON.parse(abi) as Abi).find( + (item) => item.name === func + ) as AbiFunction - const parameterSignatures = abiFunction.inputs.map((item) => item.type).join(',') + const parameterSignatures = abiFunction.inputs + .map((item) => item.type) + .join(",") return `${abiFunction.name}(${parameterSignatures})` } -export const getTotalInterval = (days: string, hours: string, minutes: string, seconds: string) => { +export const getTotalInterval = ( + days: string, + hours: string, + minutes: string, + seconds: string +) => { const d = parseInt(days) || 0 const h = parseInt(hours) || 0 const m = parseInt(minutes) || 0 @@ -100,7 +133,10 @@ export const getTotalInterval = (days: string, hours: string, minutes: string, s return s + m * 60 + h * 60 * 60 + d * 60 * 60 * 24 } -export const getTransactionUrl = (tx: ethers.ContractTransaction, chainId: number) => { +export const getTransactionUrl = ( + tx: ethers.ContractTransaction, + chainId: number +) => { const explorerUrl = GELATO_CONSTANTS.networks[chainId].explorerUrl return `${explorerUrl}/tx/${tx.hash}` @@ -112,12 +148,18 @@ export const getAddressUrl = (address: string, chainId: number) => { return `${explorerUrl}/address/${address}` } -export const sortInputsByOrder = (func: string, abi: string, inputs?: { [key: string]: string }) => { - const abiFunction = getAbiFunctions(JSON.parse(abi) as Abi).find((item) => item.name === func) as AbiFunction +export const sortInputsByOrder = ( + func: string, + abi: string, + inputs?: { [key: string]: string } +) => { + const abiFunction = getAbiFunctions(JSON.parse(abi) as Abi).find( + (item) => item.name === func + ) as AbiFunction return abiFunction.inputs.map((input) => { const value = (inputs || {})[input.name as string] - if (input.type.startsWith('bytes')) { + if (input.type.startsWith("bytes")) { return ethers.utils.toUtf8Bytes(value) } return value @@ -125,15 +167,22 @@ export const sortInputsByOrder = (func: string, abi: string, inputs?: { [key: st } export const getGqlEndpoint = (chainId: number) => { - return chainId == 1 ? '' : `-${GELATO_CONSTANTS.networks[chainId].graph}` + return chainId == 1 ? "" : `-${GELATO_CONSTANTS.networks[chainId].graph}` } -export const getTaskFunctionData = (contractAddress: string, abi: string, execDataOrSelector: string) => { +export const getTaskFunctionData = ( + contractAddress: string, + abi: string, + execDataOrSelector: string +) => { const contract = new ethers.Contract(contractAddress, abi) const functionSignature = execDataOrSelector.slice(0, 10) return { func: contract.interface.getFunction(functionSignature), - data: contract.interface.decodeFunctionData(functionSignature, execDataOrSelector), + data: contract.interface.decodeFunctionData( + functionSignature, + execDataOrSelector + ), } } diff --git a/template/integrations/gelato/core/gelato/utils/resolverDecoder.ts b/template/integrations/gelato/core/gelato/utils/resolverDecoder.ts index 573a0df..0e04b00 100644 --- a/template/integrations/gelato/core/gelato/utils/resolverDecoder.ts +++ b/template/integrations/gelato/core/gelato/utils/resolverDecoder.ts @@ -1,4 +1,4 @@ -import { ethers } from 'ethers' +import { ethers } from "ethers" export type DecodedModule = { resolverAddress: string | null @@ -32,12 +32,18 @@ export const decodeModuleArgs = (args: string[], modules: number[]) => { } export const decodeResolverArgs = (arg: string) => { - const [resolverAddress, resolverData] = ethers.utils.defaultAbiCoder.decode(['address', 'bytes'], arg) + const [resolverAddress, resolverData] = ethers.utils.defaultAbiCoder.decode( + ["address", "bytes"], + arg + ) return { resolverAddress, resolverData } } export const decodeTimeArgs = (arg: string) => { - const [startTime, interval] = ethers.utils.defaultAbiCoder.decode(['uint128', 'uint128'], arg) + const [startTime, interval] = ethers.utils.defaultAbiCoder.decode( + ["uint128", "uint128"], + arg + ) return { startTime, interval } } diff --git a/template/integrations/gelato/core/gelato/utils/types.ts b/template/integrations/gelato/core/gelato/utils/types.ts index a1d2311..fb185b7 100644 --- a/template/integrations/gelato/core/gelato/utils/types.ts +++ b/template/integrations/gelato/core/gelato/utils/types.ts @@ -1,5 +1,5 @@ -import { CreateTaskOptions } from '@gelatonetwork/automate-sdk' -import { Overrides } from 'ethers' +import { CreateTaskOptions } from "@gelatonetwork/automate-sdk" +import { Overrides } from "ethers" export type GelatoConstants = { subgraphBaseUrl: string @@ -49,6 +49,17 @@ export type FetchResolverResponse = { address: string } -export type UseNewTaskProps = { args: CreateTaskOptions; overrides?: Overrides; authToken?: string } -export type UseCancelTaskProps = { taskId: string; overrides?: Overrides | undefined } -export type UseRenameTaskProps = { taskId: string; name: string; authToken?: string | undefined } +export type UseNewTaskProps = { + args: CreateTaskOptions + overrides?: Overrides + authToken?: string +} +export type UseCancelTaskProps = { + taskId: string + overrides?: Overrides | undefined +} +export type UseRenameTaskProps = { + taskId: string + name: string + authToken?: string | undefined +} diff --git a/template/integrations/gelato/pages/gelato/layout.tsx b/template/integrations/gelato/pages/gelato/layout.tsx deleted file mode 100644 index 3350e9d..0000000 --- a/template/integrations/gelato/pages/gelato/layout.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client' -import { ReactNode } from 'react' - -import { motion } from 'framer-motion' -import Image from 'next/image' -import Balancer from 'react-wrap-balancer' - -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import { IsDarkTheme } from '@/components/shared/is-dark-theme' -import { IsLightTheme } from '@/components/shared/is-light-theme' -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { LinkComponent } from '@/components/shared/link-component' -import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' -import { turboIntegrations } from '@/data/turbo-integrations' -import { useIsAutomateSupported } from '@/integrations/gelato' - -export default function LayoutIntegration({ children }: { children: ReactNode }) { - const isAutomateSupported = useIsAutomateSupported() - - return ( - <> -
    - - - Gelato logo - - - Gelato logo - - - Gelato - - - {turboIntegrations.gelato.description} - - - - - - - - -
    - - - -
    -
    -
    - -
    - {isAutomateSupported ? children :

    Network Not Supported

    } -
    -
    -
    - - ) -} diff --git a/template/integrations/gelato/pages/gelato/opengraph-image.tsx b/template/integrations/gelato/pages/gelato/opengraph-image.tsx index 6eee191..2839be4 100644 --- a/template/integrations/gelato/pages/gelato/opengraph-image.tsx +++ b/template/integrations/gelato/pages/gelato/opengraph-image.tsx @@ -1,9 +1,9 @@ -import { IntegrationOgImage } from '@/components/ui/social/og-image-integrations' +import { IntegrationOgImage } from "@/components/ui/social/og-image-integrations" -export const runtime = 'edge' +export const runtime = "edge" export const size = { width: 1200, height: 630, } -export default IntegrationOgImage('gelato') +export default IntegrationOgImage("gelato") diff --git a/template/integrations/gelato/pages/gelato/page.tsx b/template/integrations/gelato/pages/gelato/page.tsx index 56c1b7b..70f0b45 100644 --- a/template/integrations/gelato/pages/gelato/page.tsx +++ b/template/integrations/gelato/pages/gelato/page.tsx @@ -1,18 +1,79 @@ -'use client' +"use client" -import Link from 'next/link' +import Link from "next/link" +import { turboIntegrations } from "@/data/turbo-integrations" +import { isAutomateSupported } from "@gelatonetwork/automate-sdk" +import { LuBook } from "react-icons/lu" -import { ActiveTasks } from '@/integrations/gelato' +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { + PageHeader, + PageHeaderCTA, + PageHeaderDescription, + PageHeaderHeading, +} from "@/components/layout/page-header" +import { PageSection } from "@/components/layout/page-section" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { LightDarkImage } from "@/components/shared/light-dark-image" +import { ActiveTasks, useIsAutomateSupported } from "@/integrations/gelato" + +export default function GelatoPage() { + const isAutomateSupported = useIsAutomateSupported() -export default function PageIntegration() { return ( - <> -
    - - Create Task - -
    - - +
    + + + Gelato + + Enabling developers to create augmented smart contracts that are + automated, gasless & off-chain aware + + + + + Documentation + + + + + + + + +
    + {isAutomateSupported ? ( + <> +
    + + Create Task + +
    + + + ) : ( +

    Network Not Supported

    + )} +
    +
    +
    +
    ) } diff --git a/template/integrations/gelato/pages/gelato/tasks/[id]/page.tsx b/template/integrations/gelato/pages/gelato/tasks/[id]/page.tsx index aa684f6..a1c242c 100644 --- a/template/integrations/gelato/pages/gelato/tasks/[id]/page.tsx +++ b/template/integrations/gelato/pages/gelato/tasks/[id]/page.tsx @@ -1,7 +1,11 @@ -'use client' +"use client" -import { TaskView } from '@/integrations/gelato' +import { TaskView } from "@/integrations/gelato" -export default function PageIntegration({ params }: { params: { id: string } }) { +export default function PageIntegration({ + params, +}: { + params: { id: string } +}) { return } diff --git a/template/integrations/gelato/pages/gelato/tasks/create/page.tsx b/template/integrations/gelato/pages/gelato/tasks/create/page.tsx index d43fd45..b8cd73d 100644 --- a/template/integrations/gelato/pages/gelato/tasks/create/page.tsx +++ b/template/integrations/gelato/pages/gelato/tasks/create/page.tsx @@ -1,15 +1,18 @@ -'use client' +"use client" -import Link from 'next/link' -import { FiChevronLeft } from 'react-icons/fi' +import Link from "next/link" +import { FiChevronLeft } from "react-icons/fi" -import { CreateTask } from '@/integrations/gelato' +import { CreateTask } from "@/integrations/gelato" export default function PageIntegration() { return ( -
    +
    - + Cancel diff --git a/template/integrations/gelato/pages/gelato/tasks/page.tsx b/template/integrations/gelato/pages/gelato/tasks/page.tsx index 45d4c7d..1330af5 100644 --- a/template/integrations/gelato/pages/gelato/tasks/page.tsx +++ b/template/integrations/gelato/pages/gelato/tasks/page.tsx @@ -1,5 +1,5 @@ -import { redirect } from 'next/navigation' +import { redirect } from "next/navigation" export default function PageIntegration() { - redirect('/integration/gelato/tasks/create') + redirect("/integration/gelato/tasks/create") } diff --git a/template/integrations/gelato/pages/gelato/twitter-image.tsx b/template/integrations/gelato/pages/gelato/twitter-image.tsx index dbca541..215a2a5 100644 --- a/template/integrations/gelato/pages/gelato/twitter-image.tsx +++ b/template/integrations/gelato/pages/gelato/twitter-image.tsx @@ -1,6 +1,6 @@ -import Image from './opengraph-image' +import Image from "./opengraph-image" -export const runtime = 'edge' +export const runtime = "edge" export const size = { width: 1200, height: 630, diff --git a/template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/score/route.ts b/template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/score/route.ts new file mode 100644 index 0000000..de9ee6c --- /dev/null +++ b/template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/score/route.ts @@ -0,0 +1 @@ +export { GET } from "@/integrations/gitcoin-passport/api/address-score" diff --git a/template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/stamps/route.ts b/template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/stamps/route.ts new file mode 100644 index 0000000..1ec317d --- /dev/null +++ b/template/integrations/gitcoin-passport/api/gitcoin-passport/[address]/stamps/route.ts @@ -0,0 +1 @@ +export { GET } from "@/integrations/gitcoin-passport/api/address-stamps" diff --git a/template/integrations/gitcoin-passport/api/gitcoin-passport/signing-message/route.ts b/template/integrations/gitcoin-passport/api/gitcoin-passport/signing-message/route.ts new file mode 100644 index 0000000..29780a0 --- /dev/null +++ b/template/integrations/gitcoin-passport/api/gitcoin-passport/signing-message/route.ts @@ -0,0 +1 @@ +export { GET } from "@/integrations/gitcoin-passport/api/signing-message" diff --git a/template/integrations/gitcoin-passport/api/gitcoin-passport/stamps-metadata/route.ts b/template/integrations/gitcoin-passport/api/gitcoin-passport/stamps-metadata/route.ts new file mode 100644 index 0000000..0e1f89e --- /dev/null +++ b/template/integrations/gitcoin-passport/api/gitcoin-passport/stamps-metadata/route.ts @@ -0,0 +1 @@ +export { GET } from "@/integrations/gitcoin-passport/api/stamps-metadata" diff --git a/template/integrations/gitcoin-passport/api/gitcoin-passport/submit-passport/route.ts b/template/integrations/gitcoin-passport/api/gitcoin-passport/submit-passport/route.ts new file mode 100644 index 0000000..4e74d14 --- /dev/null +++ b/template/integrations/gitcoin-passport/api/gitcoin-passport/submit-passport/route.ts @@ -0,0 +1 @@ +export { POST } from "@/integrations/gitcoin-passport/api/submit-passport" diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/README.md b/template/integrations/gitcoin-passport/core/gitcoin-passport/README.md new file mode 100644 index 0000000..f768a5c --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/README.md @@ -0,0 +1,62 @@ +# Dev guide + +### Env variables + +You need to provide two env variables in order to use this integration: + +`GITCOIN_PASSPORT_API_KEY` and `GITCOIN_PASSPORT_SCORER_ID` + +# Getting Access: Scorer ID and API Key + +The Passport API provides programmatic access to a wallet's Passport score. Once you have your API key, you need to include it with each request you make to the API. This allows Gitcoin to identify your app and verify that you are authorized to access the API. + +### Getting Your API Key + +1. **Log in to the developer portal:** Go to [scorer.gitcoin.co](https://www.scorer.gitcoin.co/) and log in to your account by connecting your wallet. +2. **Navigate to the API Keys section:** After logging in, go to the "API Keys" section. +3. **Generate an API key:** Click on the "+ Create a Key" button to generate a unique API key for your account. +4. **Store your API key securely:** Store your API key in a secure place, as it will be used to access the Passport API. + +### Scorers and Scorer ID + +A Scorer is an individual object with a unique ID that is associated with your account. If you are using the Gitcoin Passport API in multiple applications, you can set up separate communities for each one. This allows you to customize the scoring rules for each application and deduplicate any identical Passport VCs that are submitted to the same application. + +By using communities, you can manage specific parameter settings and log traffic for your Passport-enabled applications. This can help you ensure that the identity verification functionality is working correctly and meets the needs of your stakeholders. + +#### Getting your Scorer ID + +1. **Log in to the Developer Portal:** Go to [scorer.gitcoin.co](https://www.scorer.gitcoin.co/) and log in to your account by connecting your wallet. +2. **Navigate to the Communities section:** After logging in, go to the "Communities" section +3. **Create a Scorer:** Click on the "+ Create a Scorer" button and input a Scorer name and description. +4. **Find your Scorer ID:** Click on the newly created Scorer and you will see the Scorer ID in the page URL.\ + Example: `https://www.scorer.gitcoin.co/dashboard/scorer/scorer_id` + +## File Structure + +``` +integrations/gitcoin-passport +├─ api/ +│ ├─ address-score.ts +│ ├─ address-stamps.ts +│ ├─ signing-message.ts +│ ├─ stamps-metadata.ts +│ ├─ submit-passport.ts +├─ components/ +│ ├─ list-stamps.tsx +│ ├─ score-gate.tsx +│ ├─ stamp-card.tsx +│ ├─ stamp-gate.tsx +│ ├─ submit-passport-button.tsx +├─ hooks/ +│ ├─ use-get-address-stamps.ts +│ ├─ use-get-score.ts +│ ├─ use-get-stamps-metadata.ts +│ ├─ use-submit-passport.ts +├─ utils/ +│ ├─ config.ts +│ ├─ data-types.ts +│ ├─ types.ts +├─ constants.ts +├─ types.ts +├─ README.md +``` diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-score.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-score.ts new file mode 100644 index 0000000..97a247d --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-score.ts @@ -0,0 +1,36 @@ +import { env } from "@/env.mjs" + +import { GITCOIN_API_BASE_URL } from "../utils/constants" + +export async function GET( + req: Request, + { params }: { params: { address: string } } +) { + if (!env.GITCOIN_PASSPORT_API_KEY) + return new Response( + JSON.stringify({ detail: "Gitcoin passport api key not provided." }), + { + status: 400, + } + ) + + if (!env.GITCOIN_PASSPORT_SCORER_ID) + return new Response( + JSON.stringify({ + detail: "Gitcoin passport scorer (community) id not provided.", + }), + { + status: 400, + } + ) + + return await fetch( + `${GITCOIN_API_BASE_URL}/score/${env.GITCOIN_PASSPORT_SCORER_ID}/${params.address}`, + { + headers: { + "Content-Type": "application/json", + "X-API-Key": env.GITCOIN_PASSPORT_API_KEY, + }, + } + ) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-stamps.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-stamps.ts new file mode 100644 index 0000000..8a58ba6 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/address-stamps.ts @@ -0,0 +1,26 @@ +import { env } from "@/env.mjs" + +import { GITCOIN_API_BASE_URL } from "../utils/constants" + +export async function GET( + req: Request, + { params }: { params: { address: string } } +) { + if (!env.GITCOIN_PASSPORT_API_KEY) + return new Response( + JSON.stringify({ detail: "Gitcoin passport api key not provided." }), + { + status: 400, + } + ) + + return await fetch( + `${GITCOIN_API_BASE_URL}/stamps/${params.address}?include_metadata=true`, + { + headers: { + "Content-Type": "application/json", + "X-API-Key": env.GITCOIN_PASSPORT_API_KEY, + }, + } + ) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/api/signing-message.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/signing-message.ts new file mode 100644 index 0000000..a973684 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/signing-message.ts @@ -0,0 +1,20 @@ +import { env } from "@/env.mjs" + +import { GITCOIN_API_BASE_URL } from "../utils/constants" + +export async function GET(req: Request) { + if (!env.GITCOIN_PASSPORT_API_KEY) + return new Response( + JSON.stringify({ detail: "Gitcoin passport api key not provided." }), + { + status: 400, + } + ) + + return await fetch(`${GITCOIN_API_BASE_URL}/signing-message`, { + headers: { + "Content-Type": "application/json", + "X-API-Key": env.GITCOIN_PASSPORT_API_KEY, + }, + }) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/api/stamps-metadata.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/stamps-metadata.ts new file mode 100644 index 0000000..05bec8f --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/stamps-metadata.ts @@ -0,0 +1,20 @@ +import { env } from "@/env.mjs" + +import { GITCOIN_API_BASE_URL } from "../utils/constants" + +export async function GET(req: Request) { + if (!env.GITCOIN_PASSPORT_API_KEY) + return new Response( + JSON.stringify({ detail: "Gitcoin passport api key not provided." }), + { + status: 400, + } + ) + + return await fetch(`${GITCOIN_API_BASE_URL}/stamp-metadata`, { + headers: { + "Content-Type": "application/json", + "X-API-Key": env.GITCOIN_PASSPORT_API_KEY, + }, + }) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/api/submit-passport.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/submit-passport.ts new file mode 100644 index 0000000..7779077 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/api/submit-passport.ts @@ -0,0 +1,48 @@ +import { env } from "@/env.mjs" +import { z } from "zod" + +import { GITCOIN_API_BASE_URL } from "../utils/constants" + +const submitPassportSchema = z.object({ + address: z.string(), + signature: z.string(), + nonce: z.string(), +}) + +export async function POST(req: Request) { + if (!env.GITCOIN_PASSPORT_API_KEY) + return new Response( + JSON.stringify({ detail: "Gitcoin passport api key not provided." }), + { + status: 400, + } + ) + + if (!env.GITCOIN_PASSPORT_SCORER_ID) + return new Response( + JSON.stringify({ + detail: "Gitcoin passport scorer (community) id not provided.", + }), + { + status: 400, + } + ) + + const { signature, nonce, address } = submitPassportSchema.parse( + await req.json() + ) + + return await fetch(`${GITCOIN_API_BASE_URL}/submit-passport`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-API-Key": env.GITCOIN_PASSPORT_API_KEY, + }, + body: JSON.stringify({ + address, + community: env.GITCOIN_PASSPORT_SCORER_ID, + signature, + nonce, + }), + }) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/components/list-stamps.tsx b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/list-stamps.tsx new file mode 100644 index 0000000..878e591 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/list-stamps.tsx @@ -0,0 +1,148 @@ +import moment from "moment" +import { BiInfoCircle } from "react-icons/bi" +import { FiRefreshCcw } from "react-icons/fi" + +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +import { useGetAddressStamps } from "../hooks/use-get-address-stamps" +import { useGetScore } from "../hooks/use-get-score" +import { useGetStampsMetadata } from "../hooks/use-get-stamps-metadata" +import { HAS_NOT_SUBMITTED_PASSPORT_YET_ERROR } from "../utils/constants" +import { StampCard } from "./stamp-card" +import { SubmitPassportButton } from "./submit-passport-button" + +const SCORE_INFO_TEXT = + "Make sure to hit the Submit Passport button after you claimed any new stamps to update your score." + +export const ListStamps = () => { + const { stamps, isLoading: stampsLoading } = useGetStampsMetadata() + + const { + data: scoreData, + isLoading: scoreLoading, + error: scoreError, + refetch: scoreRefetch, + } = useGetScore() + + const { + stamps: addressStamps, + isLoading: addressStampsLoading, + refetch: addressStampsRefetch, + } = useGetAddressStamps() + + return ( +
    +
    +
    + <> +

    Passport

    +
    + By collecting “stamps” of validation for your identity and online + reputation, you can gain access to the most trustworthy web3 + experiences and maximize your ability to benefit from platforms + like Gitcoin Grants. The more you verify your identity, the more + opportunities you will have to vote and participate across the + web3. +
    + +
    +
    Score:
    + {scoreLoading ? ( + + ) : scoreError ? ( + + {String(scoreError)} + + ) : ( + scoreData?.score && ( + <> + + {parseFloat(scoreData.score).toFixed(2)} + + + + + + + + + {SCORE_INFO_TEXT} + + + + ) + )} +
    + {scoreError && + String(scoreError) === HAS_NOT_SUBMITTED_PASSPORT_YET_ERROR && ( + + This usually means you have not submitted your passport for + scoring yet, please hit the{" "} + Submit Passport for Scoring{" "} + button to calculate your score. + + )} + {scoreData?.last_score_timestamp && ( +
    + + Last submitted at: + + + {scoreData ? ( + moment(scoreData.last_score_timestamp).format( + "HH:mm DD MMM YYYY" + ) + ) : ( + + )} + +
    + )} + +
    +
    +
    +

    All Stamps

    + {!addressStampsLoading && ( + addressStampsRefetch()} + > + + + )} +
    +
    + {stamps + ? stamps.map((stamp) => ( + + )) + : Array(6) + .fill(0) + .map((_, idx) => )} +
    +
    + {SCORE_INFO_TEXT} +
    +
    + ) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/components/score-gate.tsx b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/score-gate.tsx new file mode 100644 index 0000000..652db6e --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/score-gate.tsx @@ -0,0 +1,49 @@ +import Link from "next/link" +import { FaLock } from "react-icons/fa" + +import { Skeleton } from "@/components/ui/skeleton" + +import { useGetScore } from "../hooks/use-get-score" +import { HAS_NOT_SUBMITTED_PASSPORT_YET_ERROR } from "../utils/constants" +import { ScoreGateProps } from "../utils/types" +import { SubmitPassportButton } from "./submit-passport-button" + +export const ScoreGate = ({ score, children, fallback }: ScoreGateProps) => { + const { data, error, isLoading, refetch } = useGetScore() + if (isLoading) return + if (error) + return ( +
    +
    {String(error)}
    + {String(error) === HAS_NOT_SUBMITTED_PASSPORT_YET_ERROR && ( + <> + + This usually means you have not submitted your passport for + scoring yet, please hit the{" "} + Submit Passport for Scoring{" "} + button to calculate your score. + + + + )} +
    + ) + if (data?.score && parseFloat(data.score) >= score) return <>{children} + return ( + <> + {fallback ?? ( +
    + + + You need a passport with the score of{" "} + + {score} + {" "} + or above to view this page. You can get more score by claiming more + stamps here. + +
    + )} + + ) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-card.tsx b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-card.tsx new file mode 100644 index 0000000..2d07662 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-card.tsx @@ -0,0 +1,210 @@ +import Image from "next/image" +import { AiOutlineCheck, AiOutlineCheckCircle } from "react-icons/ai" +import { BiInfoCircle } from "react-icons/bi" +import { FaExternalLinkAlt } from "react-icons/fa" +import { FiCircle } from "react-icons/fi" + +import { cn } from "@/lib/utils" +import { Button, buttonVariants } from "@/components/ui/button" +import { + Card, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + Dialog, + DialogContent, + DialogFooter, + DialogTrigger, +} from "@/components/ui/dialog" +import { Skeleton } from "@/components/ui/skeleton" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" +import { LinkComponent } from "@/components/shared/link-component" + +import { AddressStamp, StampsMetadataResponse } from "../utils/types" + +// just for the demo +const WhiteLogos = ["Github", "Gitcoin", "Lens", "GuildXYZ"] + +export const StampCard = ({ + stamp, + addressStamps, + addressStampsLoading, + className, +}: { + stamp?: StampsMetadataResponse + addressStamps?: AddressStamp[] + addressStampsLoading?: boolean + className?: string +}) => { + const addressHasStamp = !stamp + ? false + : addressStamps?.find( + (addressStamp) => + addressStamp.provider === stamp.id || + addressStamp.items.includes(stamp.id) + ) + return ( + + +
    + {stamp ? ( + {stamp.id} + ) : ( + + )} +
    + + {stamp ? stamp.name : } + +
    + + {stamp ? ( + stamp.description + ) : ( +
    + + + +
    + )} +
    +
    + {addressStampsLoading || !stamp ? ( + + ) : addressHasStamp ? ( +
    + + Verified +
    + ) : ( + + {stamp.connectMessage} + + + )} +
    + + {stamp ? ( + + + + + +
    +
    + {stamp.id} +
    +
    {stamp.name}
    + + {stamp.description} + + + {stamp.groups.map((group) => ( +
    +
    + {group.name} +
    + {group.stamps.map((groupStamp) => { + const addressClaimedStamp = addressStamps?.find((stamp) => + stamp.items.includes(groupStamp.name) + ) + return ( +
    + {addressClaimedStamp ? ( + + ) : ( + + )} +
    {groupStamp.description}
    + + + + + + + + {groupStamp.name} + + +
    + ) + })} +
    + ))} +
    + + Hover on the + + to see the stamp ID. + + + + {stamp.connectMessage} + + + +
    +
    + ) : ( + + + + )} +
    +
    + ) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-gate.tsx b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-gate.tsx new file mode 100644 index 0000000..1792764 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/stamp-gate.tsx @@ -0,0 +1,50 @@ +import { FaLock } from "react-icons/fa" + +import { Skeleton } from "@/components/ui/skeleton" + +import { useGetAddressStamps } from "../hooks/use-get-address-stamps" +import { useGetStampsMetadata } from "../hooks/use-get-stamps-metadata" +import { StampGateProps } from "../utils/types" +import { StampCard } from "./stamp-card" + +export const StampGate = ({ stampId, children, fallback }: StampGateProps) => { + const { stamps, isLoading, error } = useGetAddressStamps() + const { stamps: allAvailableStamps } = useGetStampsMetadata() + const gateStamp = allAvailableStamps?.find( + (stamp) => + stamp.id === stampId || + stamp.groups.find((stampGroup) => + stampGroup.stamps.find((sgs) => sgs.name === stampId) + ) + ) + if (isLoading) return + if (error) return
    {String(error)}
    + if ( + stamps?.find( + (stamp) => stamp.provider === stampId || stamp.items.includes(stampId) + ) + ) + return <>{children} + return ( + <> + {fallback ?? ( +
    + + + Please claim{" "} + + {stampId} + {" "} + stamp to be able to view this page. + + +
    + )} + + ) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/components/submit-passport-button.tsx b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/submit-passport-button.tsx new file mode 100644 index 0000000..95c5478 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/components/submit-passport-button.tsx @@ -0,0 +1,34 @@ +import { Button } from "@/components/ui/button" + +import { useSubmitPassport } from "../hooks/use-submit-passport" + +export const SubmitPassportButton = ({ + onSuccess, +}: { + onSuccess: () => void +}) => { + const { submitPassport, isLoading } = useSubmitPassport() + return ( +
    +
    +
    Submit Passport for Scoring
    + this is simply a message-signing to verify you are the owner of the + wallet. This operation does not include any fees. once submitted your + passport will be created/updated in this community. hit the submit + button to get your passport score. +
    + +
    + ) +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-address-stamps.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-address-stamps.ts new file mode 100644 index 0000000..c9f8837 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-address-stamps.ts @@ -0,0 +1,48 @@ +import { useCallback, useEffect } from "react" +import { useQuery } from "@tanstack/react-query" +import { useAccount } from "wagmi" + +import { AddressStamp, AddressStampsResponse } from "../utils/types" + +export const useGetAddressStamps = () => { + const { address } = useAccount() + const { + isLoading, + data: stamps, + error, + refetch: refetchQuery, + isRefetching, + } = useQuery({ + refetchOnWindowFocus: false, + queryKey: ["address-stamps", address], + queryFn: async () => { + if (!address) throw new Error("No address provided.") + const response = await fetch(`/api/gitcoin-passport/${address}/stamps`) + const data = (await response.json()) as AddressStampsResponse + if (response.status === 200) { + const userStamps: AddressStamp[] = [] + data.items.forEach((userStamp) => { + const provider = userStamp.metadata.platform.id + const providerObject = userStamps.find( + (userStamp) => userStamp.provider === provider + ) + const stampId = userStamp.credential.credentialSubject.provider + if (providerObject) + providerObject.items = [...providerObject.items, stampId] + userStamps.push({ provider, items: [stampId] }) + }) + return userStamps + } + if (data.detail) throw data.detail + throw new Error(response.statusText) + }, + }) + + const refetch = useCallback(() => void refetchQuery(), [refetchQuery]) + + useEffect(() => { + refetch() + }, [address]) + + return { isLoading: isLoading || isRefetching, error, stamps, refetch } +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-score.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-score.ts new file mode 100644 index 0000000..4a33f65 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-score.ts @@ -0,0 +1,39 @@ +import { useCallback, useEffect } from "react" +import { useQuery } from "@tanstack/react-query" +import { useAccount } from "wagmi" + +import { AddressScoreResponse } from "../utils/types" + +export const useGetScore = () => { + const { address } = useAccount() + const { + isLoading, + isError, + data, + error, + isRefetching, + refetch: refetchQuery, + } = useQuery({ + refetchOnWindowFocus: false, + + queryKey: ["score", address], + queryFn: async () => { + if (!address) throw new Error("No address provided.") + const response = await fetch(`/api/gitcoin-passport/${address}/score`) + const data = await response.json() + if (response.status === 200) { + return data as AddressScoreResponse + } + if (data.detail) throw data.detail + throw new Error(response.statusText) + }, + }) + + const refetch = useCallback(() => void refetchQuery(), [refetchQuery]) + + useEffect(() => { + refetch() + }, [address]) + + return { isLoading: isLoading || isRefetching, isError, error, data, refetch } +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-stamps-metadata.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-stamps-metadata.ts new file mode 100644 index 0000000..0ab5247 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-get-stamps-metadata.ts @@ -0,0 +1,29 @@ +import { useCallback } from "react" +import { useQuery } from "@tanstack/react-query" + +import { StampsMetadataResponse } from "../utils/types" + +export const useGetStampsMetadata = () => { + const { + isLoading, + data: stamps, + error, + refetch: refetchQuery, + } = useQuery({ + refetchOnWindowFocus: false, + queryKey: ["all-stamps"], + queryFn: async () => { + const response = await fetch("/api/gitcoin-passport/stamps-metadata") + const data = await response.json() + if (response.status === 200) { + return data as StampsMetadataResponse[] + } + if (data.detail) throw data.detail + throw new Error(response.statusText) + }, + }) + + const refetch = useCallback(() => void refetchQuery(), [refetchQuery]) + + return { isLoading, error, stamps, refetch } +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-submit-passport.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-submit-passport.ts new file mode 100644 index 0000000..838e1da --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/hooks/use-submit-passport.ts @@ -0,0 +1,89 @@ +import { useEffect } from "react" +import { useMutation } from "@tanstack/react-query" +import { useAccount } from "wagmi" + +import { useToast } from "@/lib/hooks/use-toast" +import { useEthersSigner } from "@/lib/hooks/web3/use-ethers-signer" + +type SigningMessageResponse = { + message: string + nonce: string +} + +export const useSubmitPassport = () => { + const signer = useEthersSigner() + const { address } = useAccount() + const { toast, dismiss } = useToast() + const handleToast = (err: string) => { + toast({ + title: "Submit failed", + description: err, + }) + + setTimeout(() => { + dismiss() + }, 4200) + } + + const signMessageMutation = useMutation({ + mutationFn: async () => { + const response = await fetch("/api/gitcoin-passport/signing-message") + const data = await response.json() + if (response.status === 200) { + return data as SigningMessageResponse + } + if (data.detail) throw data.detail + throw new Error(response.statusText) + }, + }) + + const submitPassportMutation = useMutation({ + mutationFn: async ({ + signature, + nonce, + }: { + signature: string + nonce: string + }) => { + if (!address) throw new Error("No address provided.") + const response = await fetch("/api/gitcoin-passport/submit-passport", { + method: "POST", + body: JSON.stringify({ + address, + signature, + nonce, + }), + }) + const data = await response.json() + if (response.status === 200) { + return { success: true } + } + if (data.detail) throw data.detail + throw new Error(response.statusText) + }, + }) + + const submitPassport = async () => { + if (!signer) throw new Error("Ethers signer not provided.") + const { message, nonce } = await signMessageMutation.mutateAsync() + const signature = await signer.signMessage(message) + const submitResult = await submitPassportMutation.mutateAsync({ + signature, + nonce, + }) + return submitResult + } + + useEffect(() => { + if (signMessageMutation.error ?? signMessageMutation.error) + handleToast( + String(signMessageMutation.error ?? signMessageMutation.error) + ) + }, [signMessageMutation.error, signMessageMutation.error]) + + return { + submitPassport, + isLoading: + signMessageMutation.isLoading || submitPassportMutation.isLoading, + } +} diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/utils/constants.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/utils/constants.ts new file mode 100644 index 0000000..a64f4a8 --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/utils/constants.ts @@ -0,0 +1,3 @@ +export const GITCOIN_API_BASE_URL = "https://api.scorer.gitcoin.co/registry" +export const HAS_NOT_SUBMITTED_PASSPORT_YET_ERROR = + "Unable to get score for provided scorer." diff --git a/template/integrations/gitcoin-passport/core/gitcoin-passport/utils/types.ts b/template/integrations/gitcoin-passport/core/gitcoin-passport/utils/types.ts new file mode 100644 index 0000000..661053c --- /dev/null +++ b/template/integrations/gitcoin-passport/core/gitcoin-passport/utils/types.ts @@ -0,0 +1,57 @@ +import { ReactNode } from "react" + +export type StampId = string + +export type AddressStampsResponse = { + items: Array<{ + credential: { + credentialSubject: { + provider: string + } + } + metadata: { + platform: { + id: StampId + } + } + }> + detail?: string +} + +export type StampsMetadataResponse = { + id: StampId + icon: string + name: string + description: string + connectMessage: string + groups: Array<{ + name: string + stamps: Array<{ + name: StampId + description: string + hash: string + }> + }> +} + +export type AddressScoreResponse = { + score: string + last_score_timestamp: string +} + +export interface ScoreGateProps { + score: number + fallback?: ReactNode + children: ReactNode +} + +export interface StampGateProps { + stampId: StampId + fallback?: ReactNode + children: ReactNode +} + +export type AddressStamp = { + provider: StampId + items: StampId[] +} diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/dev-guide/page.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/dev-guide/page.tsx new file mode 100644 index 0000000..a3b6657 --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/dev-guide/page.tsx @@ -0,0 +1,108 @@ +import { HTMLAttributes } from "react" +import Link from "next/link" +import { LuBook } from "react-icons/lu" +import Markdown from "react-markdown" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const DevReadme = ` +# Dev guide + +The Passport API provides programmatic access to a wallet's Passport score. +Once you have your API key, you need to include it with each request you make to the API. +This allows Gitcoin to identify your app and verify that you are authorized to access the API. + +## Getting Your API Key + +1. **Log in to the developer portal:** Go to [scorer.gitcoin.co](https://www.scorer.gitcoin.co/) and log in to your account by connecting your wallet. +2. **Navigate to the API Keys section:** After logging in, go to the "API Keys" section. +3. **Generate an API key:** Click on the "+ Create a Key" button to generate a unique API key for your account. +4. **Store your API key securely:** Store your API key in a secure place, as it will be used to access the Passport API. + +And finally place your API key in the \`.env\` file under the name \`GITCOIN_PASSPORT_API_KEY\`. + + +## Scorers and Scorer ID + +A Scorer is an individual object with a unique ID that is associated with your account. If you are using the Gitcoin Passport API in multiple applications, you can set up separate communities for each one. This allows you to customize the scoring rules for each application and deduplicate any identical Passport VCs that are submitted to the same application. + +By using communities, you can manage specific parameter settings and log traffic for your Passport-enabled applications. This can help you ensure that the identity verification functionality is working correctly and meets the needs of your stakeholders. + +### Getting your Scorer ID + +1. **Log in to the Developer Portal:** Go to [scorer.gitcoin.co](https://www.scorer.gitcoin.co/) and log in to your account by connecting your wallet. +2. **Navigate to the Communities section:** After logging in, go to the "Communities" section +3. **Create a Scorer:** Click on the "+ Create a Scorer" button and input a Scorer name and description. +4. **Find your Scorer ID:** Click on the newly created Scorer and you will see the Scorer ID in the page URL.\ + Example: \`https://www.scorer.gitcoin.co/dashboard/scorer/scorer_id\` + +And finally place your Scorer ID in the \`.env\` file under the name \`GITCOIN_PASSPORT_SCORER_ID\`. + +` + +export default function PageIntegration() { + return ( + <> + ) => ( +

    + ), + h2: ({ ...props }: HTMLAttributes) => ( +

    + ), + h3: ({ ...props }: HTMLAttributes) => ( +

    + ), + li: ({ ...props }: HTMLAttributes) => ( + <> +
  • + {`${(props as { index: number }).index + 1}. `} + {props.children} +
  • + + ), + p: ({ ...props }: HTMLAttributes) => ( +

    + ), + a: ({ ...props }: HTMLAttributes) => ( + + ), + + code: ({ ...props }: HTMLAttributes) => ( + + ), + }} + > + {DevReadme} + + + + See the full Documentation + + + ) +} diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/layout.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/layout.tsx new file mode 100644 index 0000000..e48579b --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/layout.tsx @@ -0,0 +1,60 @@ +"use client" + +import { ReactNode } from "react" +import Link from "next/link" +import { turboIntegrations } from "@/data/turbo-integrations" +import { LuBook } from "react-icons/lu" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { + PageHeader, + PageHeaderCTA, + PageHeaderDescription, + PageHeaderHeading, +} from "@/components/layout/page-header" +import { PageSection } from "@/components/layout/page-section" +import { LightDarkImage } from "@/components/shared/light-dark-image" + +import { SideBar } from "./sidebar" + +export default function PageIntegration({ children }: { children: ReactNode }) { + return ( +

    + ) +} diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/opengraph-image.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/opengraph-image.tsx new file mode 100644 index 0000000..5a6a814 --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/opengraph-image.tsx @@ -0,0 +1,9 @@ +import { IntegrationOgImage } from "@/components/ui/social/og-image-integrations" + +export const runtime = "edge" +export const size = { + width: 1200, + height: 630, +} + +export default IntegrationOgImage("gitcoinPassport") diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/page.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/page.tsx new file mode 100644 index 0000000..e5b60c6 --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/page.tsx @@ -0,0 +1,22 @@ +"use client" + +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { ListStamps } from "@/integrations/gitcoin-passport/components/list-stamps" + +export default function PageIntegration() { + return ( + <> + + + + +
    +
    You must connect your wallet to be able to see this page
    + +
    +
    + + ) +} diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/score-gated/page.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/score-gated/page.tsx new file mode 100644 index 0000000..d485b05 --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/score-gated/page.tsx @@ -0,0 +1,46 @@ +"use client" + +import { Card, CardContent, CardTitle } from "@/components/ui/card" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { ScoreGate } from "@/integrations/gitcoin-passport/components/score-gate" + +export default function PageIntegration() { + return ( + <> + +
    + + + + This card has a score gate, which its contents will only be + visible if you have submitted a passport with +10 score + + +
    Congrats! your passport score is above 10!
    +
    +
    +
    + + + + This card has a score gate, which its contents will only be + visible if you have submitted a passport with +30 score + + +
    Congrats! your passport score is above 30, WOW!
    +
    +
    +
    +
    +
    + +
    +
    You must connect your wallet to be able to see this page
    + +
    +
    + + ) +} diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/sidebar.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/sidebar.tsx new file mode 100644 index 0000000..dbcc6e2 --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/sidebar.tsx @@ -0,0 +1,50 @@ +"use client" + +import Link from "next/link" +import { usePathname } from "next/navigation" +import { turboIntegrations } from "@/data/turbo-integrations" + +import { cn } from "@/lib/utils" + +const gitcoinPassportBaseHref = turboIntegrations.gitcoinPassport.href + +const links = [ + { title: "Passport", href: gitcoinPassportBaseHref }, + { + title: "Score gated demo page", + href: gitcoinPassportBaseHref + "/score-gated", + }, + { + title: "Stamp gated demo page", + href: gitcoinPassportBaseHref + "/stamp-gated", + }, + { title: "Dev guide", href: gitcoinPassportBaseHref + "/dev-guide" }, +] +export const SideBar = () => { + const pathname = usePathname() + + return ( + + ) +} diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/stamp-gated/page.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/stamp-gated/page.tsx new file mode 100644 index 0000000..ebb4296 --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/stamp-gated/page.tsx @@ -0,0 +1,59 @@ +"use client" + +import { Card, CardContent, CardTitle } from "@/components/ui/card" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { StampGate } from "@/integrations/gitcoin-passport/components/stamp-gate" + +export default function PageIntegration() { + return ( + <> + + + + + This card has a stamp gate, which its contents will only be + visible if you have submitted a passport with the{" "} + + NFT + {" "} + stamp. + + + Congrats! you have the{" "} + + NFT + {" "} + stamp. + + + + + + + This card has a stamp gate, which its contents will only be + visible if you have submitted a passport with the Gitcoin's + + SelfStakingBronze + {" "} + stamp. + + +
    + Congrats! you have the{" "} + NFT stamp. +
    +
    +
    +
    +
    + +
    +
    You must connect your wallet to be able to see this page
    + +
    +
    + + ) +} diff --git a/template/integrations/gitcoin-passport/pages/gitcoin-passport/twitter-image.tsx b/template/integrations/gitcoin-passport/pages/gitcoin-passport/twitter-image.tsx new file mode 100644 index 0000000..215a2a5 --- /dev/null +++ b/template/integrations/gitcoin-passport/pages/gitcoin-passport/twitter-image.tsx @@ -0,0 +1,9 @@ +import Image from "./opengraph-image" + +export const runtime = "edge" +export const size = { + width: 1200, + height: 630, +} + +export default Image diff --git a/template/integrations/lens-protocol/core/lens-protocol/README.md b/template/integrations/lens-protocol/core/lens-protocol/README.md new file mode 100644 index 0000000..965c558 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/README.md @@ -0,0 +1,74 @@ +# Lens Protocol - TurboETH Integration + +Integrate TurboETH with the Lens Protocol using the [Lens Protocol SDK](https://github.com/lens-protocol/lens-sdk). This integration aims to enable developers to seamlessly utilize the features of the Lens Protocol. + +## Features + +- Authenticate using a Lens profile +- Generate a testnet Lens profile +- View profiles with browsing and search functionalities +- View publications and comments, also with browsing and search functionalities +- View a profile's feed, posts, and replies +- Display revenue information for a profile or publication +- Like or Mirror a publication +- Follow or Unfollow a profile + +## Components + +- `ProfileCard`: Displays the primary profile information, including the name, handle, picture, and follower/following count. + +- `PublicationCard`: Presents the primary publication details, such as metadata, date, associated profile, and count and list of likes, comments, and mirrors. + +## Hooks + +- `useCreateTestProfile()`: Hook to create a testnet profile. + +## File Structure + +``` +integrations/lens-protocol +├── components +│ ├── auth +│ │ ├── is-user-authenticated.tsx +│ │ ├── login-button.tsx +│ │ ├── logout-button.tsx +│ │ └── not-authenticated-yet.tsx +│ ├── feed.tsx +│ ├── load-more-button.tsx +│ ├── navbar.tsx +│ ├── profile +│ │ ├── address-profiles.tsx +│ │ ├── explore-profiles.tsx +│ │ ├── follow-unfollow-button.tsx +│ │ ├── owned-profiles.tsx +│ │ ├── profile-card.tsx +│ │ ├── profile-list-modal.tsx +│ │ ├── profile-publications.tsx +│ │ ├── profile-revenue.tsx +│ │ ├── profile-stats.tsx +│ │ ├── profile.tsx +│ │ └── search-profiles.tsx +│ └── publications +│ ├── actions +│ │ ├── button.tsx +│ │ ├── comment.tsx +│ │ ├── index.tsx +│ │ ├── like.tsx +│ │ └── mirror.tsx +│ ├── commnets.tsx +│ ├── explore-publications.tsx +│ ├── publication-actions-and-stats.tsx +│ ├── publication-card.tsx +│ ├── publication-revenue.tsx +│ ├── publication.tsx +│ ├── search-publications.tsx +│ └── stats +│ ├── index.tsx +│ └── stat.tsx +├── hooks +│ └── use-create-profile.ts +├── lens-provider.ts +├── utils +│ └── index.ts +├── README.md +``` diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/auth/is-user-authenticated.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/auth/is-user-authenticated.tsx new file mode 100644 index 0000000..ca41860 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/auth/is-user-authenticated.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from "react" +import { useActiveWallet } from "@lens-protocol/react-web" + +import { Skeleton } from "@/components/ui/skeleton" + +export const IsUserAuthenticated = ({ + children, + showLoading, +}: { + children: ReactNode + showLoading?: boolean +}) => { + const { data: wallet, loading } = useActiveWallet() + if (loading && showLoading) return + if (wallet && children) return <>{children} + return null +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/auth/login-button.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/auth/login-button.tsx new file mode 100644 index 0000000..529ace1 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/auth/login-button.tsx @@ -0,0 +1,44 @@ +import { useEffect } from "react" +import { useWalletLogin } from "@lens-protocol/react-web" +import { useAccount } from "wagmi" + +import { useToast } from "@/lib/hooks/use-toast" +import { Button } from "@/components/ui/button" + +export const LoginButton = () => { + const { execute: login, error: loginError, isPending } = useWalletLogin() + const { address } = useAccount() + const { toast, dismiss } = useToast() + + useEffect(() => { + if (loginError) showErrorToast(String(loginError)) + }, [loginError]) + + const showErrorToast = (loginError: string) => { + toast({ + title: "Login failed", + description: loginError, + }) + + setTimeout(() => { + dismiss() + }, 10000) + } + + const onLoginClick = async () => { + if (!address) return null + await login({ + address, + }) + } + return ( + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/auth/logout-button.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/auth/logout-button.tsx new file mode 100644 index 0000000..f89f567 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/auth/logout-button.tsx @@ -0,0 +1,18 @@ +import { useWalletLogout } from "@lens-protocol/react-web" + +import { Button } from "@/components/ui/button" + +export const LogoutButton = () => { + const { execute: logout, isPending } = useWalletLogout() + + return ( + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/auth/not-authenticated-yet.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/auth/not-authenticated-yet.tsx new file mode 100644 index 0000000..a4021b8 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/auth/not-authenticated-yet.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from "react" +import { useActiveWallet } from "@lens-protocol/react-web" + +export const NotAuthenticatedYet = ({ children }: { children: ReactNode }) => { + const { data: wallet } = useActiveWallet() + if (!wallet) return <>{children} + return null +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/feed.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/feed.tsx new file mode 100644 index 0000000..fbbb407 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/feed.tsx @@ -0,0 +1,41 @@ +import { ProfileId, useActiveProfile, useFeed } from "@lens-protocol/react-web" + +import { LoadMoreButton } from "./load-more-button" +import { + PublicationCard, + PublicationCardMode, +} from "./publications/publication-card" + +export const Feed = ({ profileId }: { profileId: ProfileId }) => { + const activeProfile = useActiveProfile() + const { data, loading, hasMore, next } = useFeed({ + observerId: activeProfile?.data?.id ?? undefined, + profileId, + limit: 10, + }) + return ( +
    +

    Feed

    + {data?.map((feedItem) => ( + + ))} + {loading && + Array(5) + .fill(0) + .map((_, index) => ( + + ))} + + {!loading && !data?.length && User feed is empty.} +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/load-more-button.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/load-more-button.tsx new file mode 100644 index 0000000..917b141 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/load-more-button.tsx @@ -0,0 +1,18 @@ +import { Button } from "@/components/ui/button" + +export const LoadMoreButton = ({ + hasMore, + loading, + onClick, +}: { + hasMore: boolean + loading: boolean + onClick: () => void +}) => + hasMore ? ( +
    + +
    + ) : null diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/navbar.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/navbar.tsx new file mode 100644 index 0000000..c4df256 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/navbar.tsx @@ -0,0 +1,116 @@ +import { useCallback, useState } from "react" +import { useRouter } from "next/navigation" +import { useActiveProfile } from "@lens-protocol/react-web" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { LinkComponent } from "@/components/shared/link-component" + +import { getProfilePictureSrc } from "../utils" +import { IsUserAuthenticated } from "./auth/is-user-authenticated" +import { LoginButton } from "./auth/login-button" +import { LogoutButton } from "./auth/logout-button" +import { NotAuthenticatedYet } from "./auth/not-authenticated-yet" + +export const Navbar = () => { + const [searchQuery, setSearchQuery] = useState("") + const router = useRouter() + const search = useCallback(() => { + router.push(`/integration/lens-protocol/search?q=${searchQuery}`) + }, [searchQuery]) + const { data: activeProfile } = useActiveProfile() + return ( + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/address-profiles.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/address-profiles.tsx new file mode 100644 index 0000000..a31b9f2 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/address-profiles.tsx @@ -0,0 +1,35 @@ +import { useActiveProfile, useProfilesOwnedBy } from "@lens-protocol/react-web" + +import { LoadMoreButton } from "../load-more-button" +import { ProfileCard } from "./profile-card" + +export const AddressProfiles = ({ address }: { address: string }) => { + const { data: activeProfile } = useActiveProfile() + const { + data: profiles, + loading, + hasMore, + next, + } = useProfilesOwnedBy({ + limit: 10, + address, + observerId: activeProfile?.id, + }) + return ( +
    +

    + {address}Profiles +

    +
    + {profiles?.map((profile) => ( + + ))} + {loading && + Array(4) + .fill(0) + .map((_, index) => )} +
    + +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/explore-profiles.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/explore-profiles.tsx new file mode 100644 index 0000000..fa05300 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/explore-profiles.tsx @@ -0,0 +1,44 @@ +import { useRouter } from "next/navigation" +import { useActiveProfile, useExploreProfiles } from "@lens-protocol/react-web" + +import { LoadMoreButton } from "../load-more-button" +import { ProfileCard } from "./profile-card" + +export const ExploreProfiles = () => { + const profile = useActiveProfile() + const { + data: profiles, + loading, + hasMore, + next, + } = useExploreProfiles({ + limit: 10, + observerId: profile?.data?.id ?? undefined, + }) + const router = useRouter() + return ( + <> +
    +

    Profiles

    +
    + {profiles?.map((profile) => ( + + router.push( + `/integration/lens-protocol/profiles/${profile.handle}` + ) + } + /> + ))} + {loading && + Array(4) + .fill(0) + .map((_, index) => )} +
    + +
    + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/follow-unfollow-button.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/follow-unfollow-button.tsx new file mode 100644 index 0000000..c1383f2 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/follow-unfollow-button.tsx @@ -0,0 +1,117 @@ +import { useEffect } from "react" +import { + FollowPolicyType, + Profile, + ProfileOwnedByMe, + useActiveProfile, + useFollow, + useProfile, + useUnfollow, +} from "@lens-protocol/react-web" + +import { useToast } from "@/lib/hooks/use-toast" +import { Button } from "@/components/ui/button" + +const UnauthorizedFollowButton = () => { + const { toast, dismiss } = useToast() + const showErrorToast = () => { + toast({ + title: "You need to login first.", + }) + + setTimeout(() => { + dismiss() + }, 10000) + } + return ( + + ) +} + +const AuthorizedFollowUnfollowButton = ({ + profile, + activeProfile, +}: { + profile: Profile + activeProfile: ProfileOwnedByMe +}) => { + const { data: followeeProfile } = useProfile({ + observerId: activeProfile.id, + profileId: profile.id, + }) + const { + execute: follow, + error, + isPending: followLoading, + } = useFollow({ follower: activeProfile, followee: profile }) + const { execute: unfollow, isPending: unfollowLoading } = useUnfollow({ + follower: activeProfile, + followee: profile, + }) + const { toast, dismiss } = useToast() + + useEffect(() => { + if (error) showErrorToast(String(error)) + }, [error]) + + const showErrorToast = (error: string) => { + toast({ + title: "Operation failed", + description: error, + }) + + setTimeout(() => { + dismiss() + }, 10000) + } + if (followeeProfile?.isFollowedByMe) + return ( + + ) + return ( + + ) +} + +export const FollowUnfollowButton = ({ profile }: { profile: Profile }) => { + const { data: activeProfile } = useActiveProfile() + if (!activeProfile) return + if (profile.ownedByMe) return null + return ( + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/owned-profiles.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/owned-profiles.tsx new file mode 100644 index 0000000..38ba561 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/owned-profiles.tsx @@ -0,0 +1,146 @@ +import { turboIntegrations } from "@/data/turbo-integrations" +import { + useActiveProfile, + useActiveProfileSwitch, + useProfilesOwnedByMe, +} from "@lens-protocol/react-web" +import { FaExternalLinkAlt } from "react-icons/fa" + +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { LightDarkImage } from "@/components/shared/light-dark-image" +import { LinkComponent } from "@/components/shared/link-component" + +import { useCreateTestProfile } from "../../hooks/use-create-profile" +import { IsUserAuthenticated } from "../auth/is-user-authenticated" +import { LoginButton } from "../auth/login-button" +import { NotAuthenticatedYet } from "../auth/not-authenticated-yet" +import { LoadMoreButton } from "../load-more-button" +import { ProfileCard } from "./profile-card" + +export const OwnedProfiles = () => { + const { + data: profiles, + loading, + hasMore, + next, + } = useProfilesOwnedByMe({ + limit: 10, + }) + const { + onSubmit, + handle, + error, + setHandle, + isPending: isCreating, + } = useCreateTestProfile() + const { data: activeProfile } = useActiveProfile() + const { execute: switchActiveProfile, isPending } = useActiveProfileSwitch() + return ( +
    +

    Owned Profiles

    + +
    + {profiles + ?.filter( + (profile, index, arr) => + arr.findIndex((p) => p.handle === profile.handle) === index + ) + .map((profile) => { + const isProfileSelected = profile.id === activeProfile?.id + return ( + + + + View + +
    + } + /> + ) + })} + {loading && + Array(4) + .fill(0) + .map((_, index) => )} + +
    + + + + +
    +

    + Create a new profile on testnet +

    +
    + setHandle(e.target.value)} + /> + {error && ( +
    + {String(error)} +
    + )} + +
    +
    +
    +
    + + + +
    + Claim a new handle on Lens + +
    + Join the waitlist +
    +
    +
    + + + +
    +
    + You need to login first to be able to see your profiles. +
    + +
    +
    +

    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-card.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-card.tsx new file mode 100644 index 0000000..a504c8b --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-card.tsx @@ -0,0 +1,86 @@ +import { ReactNode } from "react" +import { useRouter } from "next/navigation" +import { Profile } from "@lens-protocol/react-web" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" + +import { getProfilePictureSrc } from "../../utils" +import { FollowUnfollowButton } from "./follow-unfollow-button" + +export const ProfileCard = ({ + profile, + onClick, + cta, +}: { + profile: Profile | null + onClick?: () => void + cta?: ReactNode +}) => { + const router = useRouter() + return ( + { + e.stopPropagation() + if (onClick) onClick() + else if (profile) + router.push(`/integration/lens-protocol/profiles/${profile.handle}`) + }} + > + + {profile ? ( + + + + {profile.handle.substring(0, 1)} + + + ) : ( + + )} + {profile ? ( + + @{profile.handle} + + ) : ( + + )} + {profile ? ( + + {profile.name ?? profile.handle} + + ) : ( + + )} +
    + {profile ? ( + + {profile.stats.totalFollowers} + Followers + + ) : ( + + )} + {profile ? ( + + {profile.stats.totalFollowing} + Followings + + ) : ( + + )} +
    +
    + + {profile ? ( + + ) : ( + + )} + {cta && cta} + +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-list-modal.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-list-modal.tsx new file mode 100644 index 0000000..3888b0b --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-list-modal.tsx @@ -0,0 +1,105 @@ +import { ReactNode } from "react" +import { useRouter } from "next/navigation" +import { Profile } from "@lens-protocol/react-web" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTrigger, +} from "@/components/ui/dialog" +import { Skeleton } from "@/components/ui/skeleton" + +import { getProfilePictureSrc } from "../../utils" +import { LoadMoreButton } from "../load-more-button" +import { FollowUnfollowButton } from "./follow-unfollow-button" + +const ProfileRow = ({ profile }: { profile: Profile | null }) => { + const router = useRouter() + + return ( +
    +
    { + e.stopPropagation() + if (profile) + router.push(`/integration/lens-protocol/profiles/${profile.handle}`) + }} + > + {profile ? ( + + + + {profile.handle.substring(0, 1)} + + + ) : ( + + )} +
    + {profile ? ( + + {profile.name ?? profile.handle} + + ) : ( + + )} + {profile ? ( + + @{profile.handle} + + ) : ( + + )} +
    +
    + {profile ? ( + + ) : ( + + )} +
    + ) +} + +export const ProfileListModal = ({ + profiles, + trigger, + hasMore, + loading, + title, + next, +}: { + profiles: Profile[] + trigger: ReactNode + hasMore: boolean + loading: boolean + title: string + next: () => void +}) => { + return ( + + {trigger} + + +

    {title}

    +
    +
    + {profiles?.map((profile) => ( + + ))} + {loading && + Array(4) + .fill(0) + .map((_, index) => )} + + {!loading && profiles?.length === 0 && ( + This list is empty. + )} +
    +
    +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-publications.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-publications.tsx new file mode 100644 index 0000000..1c70429 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-publications.tsx @@ -0,0 +1,69 @@ +import { + Post, + ProfileId, + PublicationTypes, + useActiveProfile, + usePublications, +} from "@lens-protocol/react-web" +import { FaRetweet } from "react-icons/fa" + +import { LoadMoreButton } from "../load-more-button" +import { PublicationCard } from "../publications/publication-card" + +export const ProfilePublications = ({ + profileId, + publicationTypes, + title, +}: { + profileId: ProfileId + publicationTypes: PublicationTypes[] + title: string +}) => { + const { data: activeProfile } = useActiveProfile() + const { + data: publications, + loading, + hasMore, + next, + } = usePublications({ + profileId, + observerId: activeProfile?.id, + publicationTypes, + limit: 10, + }) + return ( +
    +

    {title}

    + {publications?.map((publication) => + publication.__typename === "Mirror" ? ( + <> +
    + + + {publication.profile.name ?? publication.profile.handle} + + Mirrored +
    + + + ) : ( + + ) + )} + {loading && + Array(5) + .fill(0) + .map((_, index) => ( + + ))} + + {publications?.length === 0 && No {title} yet.} +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-revenue.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-revenue.tsx new file mode 100644 index 0000000..f621f6c --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-revenue.tsx @@ -0,0 +1,36 @@ +import { ProfileId, useProfileFollowRevenue } from "@lens-protocol/react-web" + +import { Skeleton } from "@/components/ui/skeleton" + +export const ProfileRevenue = ({ profileId }: { profileId: ProfileId }) => { + const { data, loading: revenueLoading } = useProfileFollowRevenue({ + profileId, + }) + return ( +
    +

    Profile Revenue

    + {data?.map((revenue) => { + const { asset } = revenue.totalAmount + return ( +
    + + {revenue.totalAmount.toNumber()} + + {asset.symbol} +
    + ) + })} + {revenueLoading && ( +
    + +
    + )} + {!data?.length && ( + None yet + )} +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-stats.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-stats.tsx new file mode 100644 index 0000000..d5381a9 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile-stats.tsx @@ -0,0 +1,68 @@ +import { + Profile, + useActiveProfile, + useProfileFollowers, + useProfileFollowing, +} from "@lens-protocol/react-web" + +import { ProfileListModal } from "./profile-list-modal" + +export const ProfileStats = ({ profile }: { profile: Profile }) => { + const { data: activeProfile } = useActiveProfile() + const { + data: followers, + loading: followersLoading, + hasMore: followersHasMore, + next: followersNext, + } = useProfileFollowers({ + profileId: profile.id, + observerId: activeProfile?.id, + limit: 10, + }) + const { + data: followings, + loading: followingsLoading, + hasMore: followingsHasMore, + next: followingsNext, + } = useProfileFollowing({ + walletAddress: profile.ownedBy, + observerId: activeProfile?.id, + limit: 10, + }) + return ( +
    + + follower.wallet.defaultProfile + ? [follower.wallet.defaultProfile] + : [] + ) ?? [] + } + title="Followers" + trigger={ + + {profile.stats.totalFollowers} + Followers + + } + /> + following.profile) ?? []} + title="Followings" + trigger={ + + {profile.stats.totalFollowing} + Followings + + } + /> +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile.tsx new file mode 100644 index 0000000..5e3ba76 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/profile.tsx @@ -0,0 +1,191 @@ +import Link from "next/link" +import { + PublicationTypes, + useActiveProfile, + useProfile, +} from "@lens-protocol/react-web" + +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Skeleton } from "@/components/ui/skeleton" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + +import { getProfilePictureSrc } from "../../utils" +import { IsUserAuthenticated } from "../auth/is-user-authenticated" +import { LoginButton } from "../auth/login-button" +import { NotAuthenticatedYet } from "../auth/not-authenticated-yet" +import { Feed } from "../feed" +import { PublicationCard } from "../publications/publication-card" +import { FollowUnfollowButton } from "./follow-unfollow-button" +import { ProfilePublications } from "./profile-publications" +import { ProfileRevenue } from "./profile-revenue" +import { ProfileStats } from "./profile-stats" + +export const Profile = ({ handle }: { handle: string }) => { + const activeProfile = useActiveProfile() + const { data: profile, loading } = useProfile({ + handle, + observerId: activeProfile?.data?.id ?? undefined, + }) + + if (!loading && !profile) + return
    Profile not found!
    + return ( +
    +
    + {profile ? ( + + + + {profile.handle.substring(0, 1)} + + + ) : ( + + )} + {!profile ? ( + + ) : ( + profile.name && ( + + {profile.name} + + ) + )} + {profile ? ( + @{profile.handle} + ) : ( + + )} + {profile ? ( + + ) : ( +
    + + +
    + )} + {profile && !profile.ownedByMe && ( + + )} + {!profile ? ( +
    + + + +
    + ) : ( + profile.bio && ( + <> +
    Bio
    +
    + {profile.bio} +
    + + ) + )} +
    + {profile ? ( +
    Owned by
    + ) : ( + + )} + {profile ? ( + + {profile.ownedBy} + + ) : ( + + )} + {profile ? ( + + See all profiles + + ) : ( + + )} +
    + {profile && Object.keys(profile.attributes).length > 0 && ( +
    + {Object.entries(profile.attributes).map(([key, attribute]) => ( +
    + {key}: + + {attribute.toString()} + +
    + ))} +
    + )} + {profile ? ( + + ) : ( + + )} +
    +
    + +
    + {profile ? ( + + Feed + Posts + Replies + + ) : ( + + + Feed + Posts + Replies + + + )} +
    + {profile ? ( + <> + + + + + +
    + You need to login to see the profile feed. +
    + +
    +
    + + + + + + + + ) : ( +
    + {Array(5) + .fill(0) + .map((_, index) => ( + + ))} +
    + )} +
    +
    +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/profile/search-profiles.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/profile/search-profiles.tsx new file mode 100644 index 0000000..3656e3f --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/profile/search-profiles.tsx @@ -0,0 +1,44 @@ +import { useRouter } from "next/navigation" +import { useActiveProfile, useSearchProfiles } from "@lens-protocol/react-web" + +import { LoadMoreButton } from "../load-more-button" +import { ProfileCard } from "./profile-card" + +export const SearchProfiles = ({ query }: { query: string }) => { + const profile = useActiveProfile() + const { + data: profiles, + loading, + hasMore, + next, + } = useSearchProfiles({ + query, + limit: 10, + observerId: profile?.data?.id ?? undefined, + }) + const router = useRouter() + return ( +
    +

    Profiles

    +
    + {profiles?.map((profile) => ( + + router.push( + `/integration/lens-protocol/profiles/${profile.handle}` + ) + } + /> + ))} + {loading && + Array(4) + .fill(0) + .map((_, index) => )} +
    + + {!loading && !profiles?.length && No profiles found.} +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/button.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/button.tsx new file mode 100644 index 0000000..03c80e6 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/button.tsx @@ -0,0 +1,50 @@ +import { ReactNode } from "react" + +import { Button } from "@/components/ui/button" +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +export const ActionButton = ({ + color, + execute, + name, + hideCount, + count, + disabled, + icon, +}: { + color: string + execute: () => void + name: string + hideCount: boolean + count: number + disabled: boolean + icon: ReactNode +}) => { + const btnColor = `text-${color}-500 dark:text-${color}-300 hover:text-${color}-600 hover:dark:text-${color}-200` + return ( + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/comment.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/comment.tsx new file mode 100644 index 0000000..8c2e16c --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/comment.tsx @@ -0,0 +1,30 @@ +import { FaRegCommentAlt } from "react-icons/fa" + +import { useToast } from "@/lib/hooks/use-toast" + +import { IActionButton } from "." +import { ActionButton } from "./button" + +export const CommentButton = ({ publication, hideCount }: IActionButton) => { + const { toast, dismiss } = useToast() + const showErrorToast = () => { + toast({ + title: "Commenting on a publication is not supported currently.", + }) + + setTimeout(() => { + dismiss() + }, 10000) + } + return ( + showErrorToast()} + hideCount={hideCount} + icon={} + name="comment" + /> + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/index.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/index.tsx new file mode 100644 index 0000000..b4f1aa1 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/index.tsx @@ -0,0 +1,6 @@ +import { Comment, Post } from "@lens-protocol/react-web" + +export interface IActionButton { + publication: Post | Comment + hideCount: boolean +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/like.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/like.tsx new file mode 100644 index 0000000..e864a94 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/like.tsx @@ -0,0 +1,96 @@ +import { + Profile, + ReactionTypes, + useActiveProfile, + useReaction, +} from "@lens-protocol/react-web" +import { FaHeart, FaRegHeart } from "react-icons/fa" + +import { useToast } from "@/lib/hooks/use-toast" + +import { IActionButton } from "." +import { IsUserAuthenticated } from "../../auth/is-user-authenticated" +import { NotAuthenticatedYet } from "../../auth/not-authenticated-yet" +import { ActionButton } from "./button" + +const UnAuthorizedLikeButton = ({ publication, hideCount }: IActionButton) => { + const { toast, dismiss } = useToast() + const showErrorToast = () => { + toast({ + title: "You need to login first.", + }) + + setTimeout(() => { + dismiss() + }, 10000) + } + return ( + showErrorToast()} + hideCount={hideCount} + icon={} + name="like" + /> + ) +} + +const AuthorizedLikeButton = ({ + publication, + hideCount, + profile, +}: IActionButton & { profile: Profile }) => { + const { addReaction, removeReaction, hasReaction, isPending } = useReaction({ + profileId: profile.id, + }) + + const reactionType = ReactionTypes.Upvote + + const hasReactionType = hasReaction({ + reactionType, + publication, + }) + + const execute = async () => { + if (isPending) return + if (!hasReactionType) { + await addReaction({ + reactionType, + publication, + }) + } else if (hasReactionType) { + await removeReaction({ + reactionType, + publication, + }) + } + } + + return ( + : } + name="like" + /> + ) +} + +export const LikeButton = (props: IActionButton) => { + const { data: profile } = useActiveProfile() + return ( + <> + + + + + {profile && } + + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/mirror.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/mirror.tsx new file mode 100644 index 0000000..bbd9aa9 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/actions/mirror.tsx @@ -0,0 +1,98 @@ +import { useEffect } from "react" +import { + ProfileOwnedByMe, + useActiveProfile, + useCreateMirror, +} from "@lens-protocol/react-web" +import { FaRetweet } from "react-icons/fa" + +import { useToast } from "@/lib/hooks/use-toast" + +import { IActionButton } from "." +import { IsUserAuthenticated } from "../../auth/is-user-authenticated" +import { NotAuthenticatedYet } from "../../auth/not-authenticated-yet" +import { ActionButton } from "./button" + +const UnAuthorizedMirrorButton = ({ + publication, + hideCount, +}: IActionButton) => { + const { toast, dismiss } = useToast() + const showErrorToast = () => { + toast({ + title: "You need to login first.", + }) + + setTimeout(() => { + dismiss() + }, 10000) + } + return ( + showErrorToast()} + hideCount={hideCount} + icon={} + name="mirror" + /> + ) +} + +const AuthorizedMirrorButton = ({ + publication, + hideCount, + profile, +}: IActionButton & { profile: ProfileOwnedByMe }) => { + const { + execute: create, + isPending, + error, + } = useCreateMirror({ publisher: profile }) + const { toast, dismiss } = useToast() + const showErrorToast = (error: string) => { + toast({ + title: "Mirror failed.", + description: error, + }) + + setTimeout(() => { + dismiss() + }, 10000) + } + useEffect(() => { + if (error) showErrorToast(String(error)) + }, [error]) + return ( + } + name="mirror" + execute={() => + create({ + publication, + }) + } + /> + ) +} + +export const MirrorButton = (props: IActionButton) => { + const { data: profile } = useActiveProfile() + return ( + <> + + + + + {profile && } + + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/commnets.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/commnets.tsx new file mode 100644 index 0000000..47cea9d --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/commnets.tsx @@ -0,0 +1,46 @@ +import { PublicationId, useComments } from "@lens-protocol/react-web" + +import { LoadMoreButton } from "../load-more-button" +import { PublicationCard } from "./publication-card" + +export const Comments = ({ + publicationId, +}: { + publicationId: PublicationId +}) => { + const { + data: comments, + loading, + hasMore, + next, + } = useComments({ + commentsOf: publicationId, + limit: 10, + }) + return ( +
    + {comments?.map((comment, index) => ( + + ))} + {loading && + Array(5) + .fill(0) + .map((_, index) => ( + + ))} + +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/explore-publications.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/explore-publications.tsx new file mode 100644 index 0000000..ec35cc7 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/explore-publications.tsx @@ -0,0 +1,40 @@ +import { + Post, + PublicationTypes, + useExplorePublications, +} from "@lens-protocol/react-web" + +import { LoadMoreButton } from "../load-more-button" +import { PublicationCard } from "./publication-card" + +export const ExplorePublications = () => { + const { + data: publications, + loading, + hasMore, + next, + } = useExplorePublications({ + limit: 10, + publicationTypes: [PublicationTypes.Post], + }) + return ( + <> +
    +

    Publications

    + {publications?.map((publication) => ( + + ))} + {loading && + Array(5) + .fill(0) + .map((_, index) => ( + + ))} + +
    + + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-actions-and-stats.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-actions-and-stats.tsx new file mode 100644 index 0000000..6450d31 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-actions-and-stats.tsx @@ -0,0 +1,25 @@ +import { Comment, Post } from "@lens-protocol/react-web" + +import { CommentButton } from "./actions/comment" +import { LikeButton } from "./actions/like" +import { MirrorButton } from "./actions/mirror" +import { PublicationStats } from "./stats" + +export const PublicationActionsAndStats = ({ + publication, + showCounts = false, +}: { + publication: Post | Comment + showCounts: boolean +}) => { + return ( +
    + {showCounts && } +
    + + + +
    +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-card.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-card.tsx new file mode 100644 index 0000000..c06597e --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-card.tsx @@ -0,0 +1,243 @@ +import { ReactNode } from "react" +import { useRouter } from "next/navigation" +import { Comment, FeedItem, Post } from "@lens-protocol/react-web" +import moment from "moment" +import { FaRetweet } from "react-icons/fa" + +import { cn } from "@/lib/utils" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" +import { Card, CardContent } from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" + +import { getProfilePictureSrc } from "../../utils" +import { Comments } from "./commnets" +import { PublicationActionsAndStats } from "./publication-actions-and-stats" +import { PublicationRevenue } from "./publication-revenue" + +export enum PublicationCardMode { + Normal = "normal", + Compact = "compact", + FeedComment = "FeedComment", + Full = "full", +} + +const Wrapper = ({ + shouldLinkToFullMode, + children, + id, + classNames, + chainedStyle, + last, +}: { + shouldLinkToFullMode: boolean + children: ReactNode + id?: string + classNames: string + chainedStyle: boolean + last: boolean +}) => { + const router = useRouter() + const defaultClassName = "p-6 w-full block" + const bottomLine = + "before:absolute before:left-[32px] before:top-[40px] before:h-full before:bg-slate-200 before:dark:bg-neutral-600 before:self-start before:w-[1px]" + const topLine = + "after:absolute after:left-[32px] after:top-[0px] after:h-[24px] after:bg-slate-200 after:dark:bg-neutral-600 after:self-start after:w-[1px]" + const chainedClassName = "relative ml-[-12px]" + const baseClassName = cn( + defaultClassName, + chainedStyle && chainedClassName, + chainedStyle && !last && bottomLine, + chainedStyle && topLine + ) + if (shouldLinkToFullMode) + return ( + { + e.stopPropagation() + if (id) router.push(`/integration/lens-protocol/publications/${id}`) + }} + > + {children} + + ) + return ( + + {children} + + ) +} + +export const PublicationCard = ({ + publication, + feedItem, + mode = PublicationCardMode.Normal, + wrapperClassNames = "", + last = false, + chainedStyle = false, +}: { + publication: Post | Comment | null + feedItem?: FeedItem + mode?: PublicationCardMode + wrapperClassNames?: string + last?: boolean + chainedStyle?: boolean +}) => { + const router = useRouter() + const compactMode = mode === PublicationCardMode.Compact + const fullMode = mode === PublicationCardMode.Full + const feedCommentMode = mode === PublicationCardMode.FeedComment + const { profile } = publication ?? { profile: null } + const mirrored = feedItem?.electedMirror ?? false + const bottomLine = + "relative before:absolute before:left-[-20px] before:top-[-16px] before:h-[calc(100%_+_32px)] before:bg-slate-200 before:dark:bg-neutral-600 before:self-start before:w-[1px]" + return ( + + {mirrored && ( +
    + + + {feedItem?.electedMirror?.profile.name ?? + feedItem?.electedMirror?.profile.handle} + + Mirrored +
    + )} +
    +
    +
    { + e.stopPropagation() + if (profile) + router.push( + `/integration/lens-protocol/profiles/${profile.handle}` + ) + }} + > + {profile ? ( + + + + {profile.handle.substring(0, 1)} + + + ) : ( + + )} +
    + + {profile ? ( + profile.name ?? profile.handle + ) : ( + + )} + + {!compactMode && ( + + {profile ? ( + <>@{profile.handle} + ) : ( + + )} + + )} +
    +
    +
    +
    +
    +
    + {publication ? ( + publication.metadata?.content + ) : ( +
    + + + +
    + )} +
    +
    + {publication ? ( + moment(publication.createdAt).format("HH:mm YY MMM DD") + ) : ( + + )} +
    + {!compactMode && publication && ( + + )} + {fullMode && publication && ( + + )} +
    +
    + {fullMode ? ( + publication ? ( + + ) : ( +
    + {Array(3) + .fill(0) + .map((_, index) => ( + + ))} +
    + ) + ) : null} + {feedCommentMode && feedItem?.comments?.[0] && ( + + )} +
    +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-revenue.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-revenue.tsx new file mode 100644 index 0000000..720bad6 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication-revenue.tsx @@ -0,0 +1,29 @@ +import { PublicationId, usePublicationRevenue } from "@lens-protocol/react-web" + +export const PublicationRevenue = ({ + publicationId, +}: { + publicationId: PublicationId +}) => { + const { data, loading: revenueLoading } = usePublicationRevenue({ + publicationId, + }) + if (revenueLoading) return null + return ( +
    +

    Publication Revenue

    + {data ? ( +
    + + {data.revenue.totalAmount.toNumber()} + + + {data.revenue.totalAmount.asset.symbol} + +
    + ) : ( + None yet + )} +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication.tsx new file mode 100644 index 0000000..6d88c54 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/publication.tsx @@ -0,0 +1,115 @@ +import { + Comment, + Mirror, + Post, + PublicationId, + usePublication, +} from "@lens-protocol/react-web" +import { FaRegCommentAlt, FaRetweet } from "react-icons/fa" + +import { LinkComponent } from "@/components/shared/link-component" + +import { PublicationCard, PublicationCardMode } from "./publication-card" + +export const Publication = ({ + publicationId, +}: { + publicationId: PublicationId +}) => { + return ( +
    + +
    + ) +} + +export const PublicationDetails = ({ + publicationId, +}: { + publicationId: PublicationId +}) => { + const { data: publication, loading } = usePublication({ + publicationId, + }) + if (loading) + return ( + + ) + if (!publication) + return
    Publication not found!
    + if (publication.__typename === "Mirror") + return + if (publication.__typename === "Comment") + return + if (publication.__typename === "Post") + return + return ( +
    Unknown type of publication!
    + ) +} + +const RenderPost = ({ publication }: { publication: Post }) => { + return ( + + ) +} + +const RenderMirror = ({ publication }: { publication: Mirror }) => { + const { profile } = publication + return ( +
    +
    + + + + {profile.name ?? profile.handle} + + + Mirrored +
    + {publication.mirrorOf.__typename === "Post" && ( + + )} + {publication.mirrorOf.__typename === "Comment" && ( + + )} +
    + ) +} + +const RenderComment = ({ publication }: { publication: Comment }) => { + const { profile } = publication + return ( +
    +
    + Original publication +
    + {publication.commentOn?.__typename === "Post" && ( + + )} +
    + + + + {profile.name ?? profile.handle} + + + Commented +
    + +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/search-publications.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/search-publications.tsx new file mode 100644 index 0000000..d999f8b --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/search-publications.tsx @@ -0,0 +1,32 @@ +import { Post, useSearchPublications } from "@lens-protocol/react-web" + +import { LoadMoreButton } from "../load-more-button" +import { PublicationCard } from "./publication-card" + +export const SearchPublications = ({ query }: { query: string }) => { + const { + data: publications, + loading, + hasMore, + next, + } = useSearchPublications({ query, limit: 10 }) + return ( +
    +

    Publications

    + {publications?.map((publication) => ( + + ))} + {loading && + Array(5) + .fill(0) + .map((_, index) => ( + + ))} + + {!loading && !publications?.length && No publications found.} +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/index.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/index.tsx new file mode 100644 index 0000000..c3e0d25 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/index.tsx @@ -0,0 +1,69 @@ +import { + Comment, + Post, + useActiveProfile, + useWhoCollectedPublication, + useWhoMirroredPublication, + useWhoReacted, +} from "@lens-protocol/react-web" + +import { Stat } from "./stat" + +export const PublicationStats = ({ + publication, +}: { + publication: Post | Comment +}) => { + const { data: profile } = useActiveProfile() + const likes = useWhoReacted({ + publicationId: publication.id, + observerId: profile?.id, + limit: 10, + }) + const mirrors = useWhoMirroredPublication({ + publicationId: publication.id, + observerId: profile?.id, + limit: 10, + }) + const collects = useWhoCollectedPublication({ + publicationId: publication.id, + observerId: profile?.id, + limit: 10, + }) + return ( +
    + + + {publication.stats.commentsCount} + + comments + + reaction.profile)} + hasMore={likes.hasMore} + loading={likes.loading} + name="likes" + next={likes.next} + value={publication.stats.totalUpvotes} + /> + + + wallet.defaultProfile ? [wallet.defaultProfile] : [] + )} + hasMore={collects.hasMore} + loading={collects.loading} + name="collects" + next={collects.next} + value={publication.stats.totalAmountOfCollects} + /> +
    + ) +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/stat.tsx b/template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/stat.tsx new file mode 100644 index 0000000..6b3b0a5 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/components/publications/stats/stat.tsx @@ -0,0 +1,33 @@ +import { Profile } from "@lens-protocol/react-web" + +import { ProfileListModal } from "../../profile/profile-list-modal" + +export const Stat = ({ + name, + value, + data, + hasMore, + loading, + next, +}: { + name: string + value: number + data?: Profile[] + hasMore: boolean + loading: boolean + next: () => void +}) => ( + + {value} + {name} + + } + /> +) diff --git a/template/integrations/lens-protocol/core/lens-protocol/hooks/use-create-profile.ts b/template/integrations/lens-protocol/core/lens-protocol/hooks/use-create-profile.ts new file mode 100644 index 0000000..e05cc0f --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/hooks/use-create-profile.ts @@ -0,0 +1,23 @@ +import { FormEvent, useState } from "react" +import { isValidHandle, useCreateProfile } from "@lens-protocol/react-web" + +export const useCreateTestProfile = () => { + const [handle, setHandle] = useState(null) + const [error, setError] = useState(null) + + const { execute: create, error: createError, isPending } = useCreateProfile() + + const onSubmit = async (e: FormEvent) => { + e.preventDefault() + setError(null) + if (!handle) return + if (!isValidHandle(handle)) { + setError("Handle is taken.") + return + } + setHandle(null) + await create({ handle }) + } + + return { error: error ?? createError, isPending, onSubmit, handle, setHandle } +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/lens-provider.ts b/template/integrations/lens-protocol/core/lens-protocol/lens-provider.ts new file mode 100644 index 0000000..37c9a3b --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/lens-provider.ts @@ -0,0 +1,7 @@ +import { development, LensConfig } from "@lens-protocol/react-web" +import { bindings as wagmiBindings } from "@lens-protocol/wagmi" + +export const lensProviderConfig: LensConfig = { + bindings: wagmiBindings(), + environment: development, +} diff --git a/template/integrations/lens-protocol/core/lens-protocol/utils/index.ts b/template/integrations/lens-protocol/core/lens-protocol/utils/index.ts new file mode 100644 index 0000000..405a475 --- /dev/null +++ b/template/integrations/lens-protocol/core/lens-protocol/utils/index.ts @@ -0,0 +1,14 @@ +import { Profile } from "@lens-protocol/react-web" + +export const getProfilePictureSrc = (profile: Profile): string | undefined => { + if (!profile || !profile.picture) return undefined + if (profile.picture.__typename === "MediaSet") { + const splittedUrl = profile.picture.original.url.split("ipfs://") + if (splittedUrl.length === 2) + return `https://gateway.ipfs.io/ipfs/${splittedUrl[1]}` + return profile.picture.original.url + } + if (profile.picture.__typename === "NftImage") + return `https://cdn.stamp.fyi/avatar/eth:${profile.picture.uri}` + return undefined +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/explore/page.tsx b/template/integrations/lens-protocol/pages/lens-protocol/explore/page.tsx new file mode 100644 index 0000000..e8aa445 --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/explore/page.tsx @@ -0,0 +1,13 @@ +"use client" + +import { ExploreProfiles } from "@/integrations/lens-protocol/components/profile/explore-profiles" +import { ExplorePublications } from "@/integrations/lens-protocol/components/publications/explore-publications" + +export default function PageIntegration() { + return ( + <> + + + + ) +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/layout.tsx b/template/integrations/lens-protocol/pages/lens-protocol/layout.tsx new file mode 100644 index 0000000..1e851aa --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/layout.tsx @@ -0,0 +1,75 @@ +"use client" + +import { ReactNode } from "react" +import Link from "next/link" +import { turboIntegrations } from "@/data/turbo-integrations" +import { LensProvider } from "@lens-protocol/react-web" +import { LuBook } from "react-icons/lu" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { + PageHeader, + PageHeaderCTA, + PageHeaderDescription, + PageHeaderHeading, +} from "@/components/layout/page-header" +import { PageSection } from "@/components/layout/page-section" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" +import { LightDarkImage } from "@/components/shared/light-dark-image" +import { Navbar } from "@/integrations/lens-protocol/components/navbar" +import { lensProviderConfig } from "@/integrations/lens-protocol/lens-provider" + +export default function LayoutIntegration({ + children, +}: { + children: ReactNode +}) { + return ( + <> + + + + {turboIntegrations.lensProtocol.name} + + + {turboIntegrations.lensProtocol.description} + + + + + Documentation + + + + + + + + + +
    + +
    + {children} +
    +
    +
    +
    +
    + + ) +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/opengraph-image.tsx b/template/integrations/lens-protocol/pages/lens-protocol/opengraph-image.tsx new file mode 100644 index 0000000..657902a --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/opengraph-image.tsx @@ -0,0 +1,9 @@ +import { IntegrationOgImage } from "@/components/ui/social/og-image-integrations" + +export const runtime = "edge" +export const size = { + width: 1200, + height: 630, +} + +export default IntegrationOgImage("lensProtocol") diff --git a/template/integrations/lens-protocol/pages/lens-protocol/page.tsx b/template/integrations/lens-protocol/pages/lens-protocol/page.tsx new file mode 100644 index 0000000..bbcc659 --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation" + +export default function PageIntegration() { + redirect("/integration/lens-protocol/profiles") +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/profiles/[handle]/page.tsx b/template/integrations/lens-protocol/pages/lens-protocol/profiles/[handle]/page.tsx new file mode 100644 index 0000000..539ac4e --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/profiles/[handle]/page.tsx @@ -0,0 +1,11 @@ +"use client" + +import { Profile } from "@/integrations/lens-protocol/components/profile/profile" + +export default function PageIntegration({ + params, +}: { + params: { handle: string } +}) { + return +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/profiles/address/[address]/page.tsx b/template/integrations/lens-protocol/pages/lens-protocol/profiles/address/[address]/page.tsx new file mode 100644 index 0000000..8eccd9b --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/profiles/address/[address]/page.tsx @@ -0,0 +1,11 @@ +"use client" + +import { AddressProfiles } from "@/integrations/lens-protocol/components/profile/address-profiles" + +export default function PageIntegration({ + params, +}: { + params: { address: string } +}) { + return +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/profiles/page.tsx b/template/integrations/lens-protocol/pages/lens-protocol/profiles/page.tsx new file mode 100644 index 0000000..f73d62c --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/profiles/page.tsx @@ -0,0 +1,7 @@ +"use client" + +import { OwnedProfiles } from "@/integrations/lens-protocol/components/profile/owned-profiles" + +export default function PageIntegration() { + return +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/publications/[id]/page.tsx b/template/integrations/lens-protocol/pages/lens-protocol/publications/[id]/page.tsx new file mode 100644 index 0000000..a9c16c3 --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/publications/[id]/page.tsx @@ -0,0 +1,13 @@ +"use client" + +import { PublicationId } from "@lens-protocol/react-web" + +import { Publication } from "@/integrations/lens-protocol/components/publications/publication" + +export default function PageIntegration({ + params, +}: { + params: { id: string } +}) { + return +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/search/page.tsx b/template/integrations/lens-protocol/pages/lens-protocol/search/page.tsx new file mode 100644 index 0000000..cf71d56 --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/search/page.tsx @@ -0,0 +1,17 @@ +"use client" + +import { useSearchParams } from "next/navigation" + +import { SearchProfiles } from "@/integrations/lens-protocol/components/profile/search-profiles" +import { SearchPublications } from "@/integrations/lens-protocol/components/publications/search-publications" + +export default function PageIntegration() { + const searchParams = useSearchParams() + const query = searchParams.get("q") ?? "" + return ( + <> + + + + ) +} diff --git a/template/integrations/lens-protocol/pages/lens-protocol/twitter-image.tsx b/template/integrations/lens-protocol/pages/lens-protocol/twitter-image.tsx new file mode 100644 index 0000000..215a2a5 --- /dev/null +++ b/template/integrations/lens-protocol/pages/lens-protocol/twitter-image.tsx @@ -0,0 +1,9 @@ +import Image from "./opengraph-image" + +export const runtime = "edge" +export const size = { + width: 1200, + height: 630, +} + +export default Image diff --git a/template/integrations/lit-protocol/api/lit-protocol/[id]/route.ts b/template/integrations/lit-protocol/api/lit-protocol/[id]/route.ts index f43118f..485f225 100644 --- a/template/integrations/lit-protocol/api/lit-protocol/[id]/route.ts +++ b/template/integrations/lit-protocol/api/lit-protocol/[id]/route.ts @@ -1 +1 @@ -export { GET } from '@/integrations/lit-protocol/api/[id]' +export { GET } from "@/integrations/lit-protocol/api/[id]" diff --git a/template/integrations/lit-protocol/api/lit-protocol/encrypt/route.ts b/template/integrations/lit-protocol/api/lit-protocol/encrypt/route.ts index 294e399..c2f4e58 100644 --- a/template/integrations/lit-protocol/api/lit-protocol/encrypt/route.ts +++ b/template/integrations/lit-protocol/api/lit-protocol/encrypt/route.ts @@ -1 +1 @@ -export { POST } from '@/integrations/lit-protocol/api/encrypt' +export { POST } from "@/integrations/lit-protocol/api/encrypt" diff --git a/template/integrations/lit-protocol/core/lit-protocol/api/[id].ts b/template/integrations/lit-protocol/core/lit-protocol/api/[id].ts index 8667230..ce4631d 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/api/[id].ts +++ b/template/integrations/lit-protocol/core/lit-protocol/api/[id].ts @@ -1,9 +1,13 @@ -import { env } from '@/env.mjs' -import { prisma } from '@/lib/prisma' +import { env } from "@/env.mjs" -export async function GET(req: Request, { params }: { params: { id: string } }) { +import { prisma } from "@/lib/prisma" + +export async function GET( + req: Request, + { params }: { params: { id: string } } +) { try { - if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set') + if (!env.DATABASE_URL) throw new Error("DATABASE_URL is not set") const id = params.id const message = await prisma.litProtocolMessage.findFirst({ @@ -13,10 +17,13 @@ export async function GET(req: Request, { params }: { params: { id: string } }) }) if (!message) { - return new Response('Message not found', { status: 404 }) + return new Response("Message not found", { status: 404 }) } - return new Response(JSON.stringify(message), { status: 200, headers: { 'Content-Type': 'application/json' } }) + return new Response(JSON.stringify(message), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e) console.log(errorMessage) diff --git a/template/integrations/lit-protocol/core/lit-protocol/api/encrypt.ts b/template/integrations/lit-protocol/core/lit-protocol/api/encrypt.ts index d96c3bc..67d9ad4 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/api/encrypt.ts +++ b/template/integrations/lit-protocol/core/lit-protocol/api/encrypt.ts @@ -1,7 +1,7 @@ -import { z } from 'zod' +import { env } from "@/env.mjs" +import { z } from "zod" -import { env } from '@/env.mjs' -import { prisma } from '@/lib/prisma' +import { prisma } from "@/lib/prisma" const encryptSchema = z.object({ encryptedString: z.string(), @@ -11,15 +11,26 @@ const encryptSchema = z.object({ export async function POST(req: Request) { try { - if (!env.DATABASE_URL) throw new Error('DATABASE_URL not set') + if (!env.DATABASE_URL) throw new Error("DATABASE_URL not set") - const { encryptedString, accessControlConditions, encryptedSymmetricKeyString } = encryptSchema.parse(await req.json()) + const { + encryptedString, + accessControlConditions, + encryptedSymmetricKeyString, + } = encryptSchema.parse(await req.json()) - if (!encryptedString || !accessControlConditions || !encryptedSymmetricKeyString) { - return new Response(JSON.stringify({ ok: false, error: 'Invalid parameters' }), { - status: 400, - headers: { 'Content-Type': 'application/json' }, - }) + if ( + !encryptedString || + !accessControlConditions || + !encryptedSymmetricKeyString + ) { + return new Response( + JSON.stringify({ ok: false, error: "Invalid parameters" }), + { + status: 400, + headers: { "Content-Type": "application/json" }, + } + ) } const litProtocolMessage = await prisma.litProtocolMessage.create({ @@ -32,10 +43,16 @@ export async function POST(req: Request) { }, }) - return new Response(JSON.stringify(litProtocolMessage), { status: 200, headers: { 'Content-Type': 'application/json' } }) + return new Response(JSON.stringify(litProtocolMessage), { + status: 200, + headers: { "Content-Type": "application/json" }, + }) } catch (e) { const errorMessage = e instanceof Error ? e.message : String(e) console.error(errorMessage) - return new Response(JSON.stringify({ ok: false, error: errorMessage }), { status: 500, headers: { 'Content-Type': 'application/json' } }) + return new Response(JSON.stringify({ ok: false, error: errorMessage }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }) } } diff --git a/template/integrations/lit-protocol/core/lit-protocol/client.ts b/template/integrations/lit-protocol/core/lit-protocol/client.ts index c5f3e2f..03664d2 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/client.ts +++ b/template/integrations/lit-protocol/core/lit-protocol/client.ts @@ -1,4 +1,4 @@ -import * as LitJsSdk from '@lit-protocol/lit-node-client' +import * as LitJsSdk from "@lit-protocol/lit-node-client" /** * This is a wrapper around the Lit Node Client SDK. diff --git a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-address.tsx b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-address.tsx index c12e689..846b734 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-address.tsx +++ b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-address.tsx @@ -1,16 +1,19 @@ -import { type ChangeEvent, useState } from 'react' +import { useState, type ChangeEvent } from "react" +import { useForm } from "react-hook-form" +import { isAddress } from "viem" -import { useForm } from 'react-hook-form' -import { isAddress } from 'viem' +import { Button } from "@/components/ui/button" -import { AccessControlProps } from './types' +import { AccessControlProps } from "./types" interface FormSchema { address: string } -export function AccessControlSingleAddress({ setAccessControlConditions }: AccessControlProps) { - const [address, setAddress] = useState('') +export function AccessControlSingleAddress({ + setAccessControlConditions, +}: AccessControlProps) { + const [address, setAddress] = useState("") const { register, @@ -31,10 +34,11 @@ export function AccessControlSingleAddress({ setAccessControlConditions }: Acces isAddress(value) || 'Invalid Ethereum address', + isValidEthereumAddress: (value) => + isAddress(value) || "Invalid Ethereum address", }, })} placeholder="0x1234567890123456789012345678901234567890" @@ -42,10 +46,14 @@ export function AccessControlSingleAddress({ setAccessControlConditions }: Acces value={address} onChange={handleChange} /> - {errors.address &&

    {String(errors.address?.message)}

    } - + ) } @@ -53,14 +61,14 @@ export function AccessControlSingleAddress({ setAccessControlConditions }: Acces const getAccessControlConditions = (address: string) => { return [ { - conditionType: 'evmBasic', - contractAddress: '', - standardContractType: '', - chain: 'ethereum', - method: '', - parameters: [':userAddress'], + conditionType: "evmBasic", + contractAddress: "", + standardContractType: "", + chain: "ethereum", + method: "", + parameters: [":userAddress"], returnValueTest: { - comparator: '=', + comparator: "=", value: address, }, }, diff --git a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-erc721.tsx b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-erc721.tsx index 782ae18..f46f29c 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-erc721.tsx +++ b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-single-erc721.tsx @@ -1,12 +1,18 @@ -import { useState } from 'react' +import { useState } from "react" +import { useForm } from "react-hook-form" +import { isAddress } from "viem" -import { useForm } from 'react-hook-form' -import { isAddress } from 'viem' +import { Button } from "@/components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' - -import { AccessControlProps } from './types' -import { supportedChains } from '../../utils/config' +import { supportedChains } from "../../utils/config" +import { AccessControlProps } from "./types" interface FormSchema { address: string @@ -14,10 +20,12 @@ interface FormSchema { chain: string } -export function AccessControlSingleERC721({ setAccessControlConditions }: AccessControlProps) { - const [address, setAddress] = useState('') +export function AccessControlSingleERC721({ + setAccessControlConditions, +}: AccessControlProps) { + const [address, setAddress] = useState("") const [tokenId, setTokenId] = useState() - const [chain, setChain] = useState('ethereum') + const [chain, setChain] = useState("ethereum") const { register, @@ -26,7 +34,9 @@ export function AccessControlSingleERC721({ setAccessControlConditions }: Access } = useForm() const onSubmit = (data: FormSchema) => { - setAccessControlConditions(getAccessControlConditions(chain, data.address, data.tokenId)) + setAccessControlConditions( + getAccessControlConditions(chain, data.address, data.tokenId) + ) } return ( @@ -34,11 +44,15 @@ export function AccessControlSingleERC721({ setAccessControlConditions }: Access
    - setChain(value)} + > + - + {supportedChains.map((chain) => ( {chain} @@ -51,10 +65,11 @@ export function AccessControlSingleERC721({ setAccessControlConditions }: Access isAddress(value) || 'Invalid Contract address', + isValidEthereumAddress: (value) => + isAddress(value) || "Invalid Contract address", }, })} placeholder="0x1234567890123456789012345678901234567890" @@ -62,7 +77,11 @@ export function AccessControlSingleERC721({ setAccessControlConditions }: Access value={address} onChange={(e) => setAddress(e.target.value)} /> - {errors.address &&

    {String(errors.address?.message)}

    } + {errors.address && ( +

    + {String(errors.address?.message)} +

    + )}
    @@ -71,8 +90,8 @@ export function AccessControlSingleERC721({ setAccessControlConditions }: Access min={0} placeholder="0" type="number" - {...register('tokenId', { - required: 'Token ID is required', + {...register("tokenId", { + required: "Token ID is required", })} value={tokenId} onChange={(e) => { @@ -80,28 +99,36 @@ export function AccessControlSingleERC721({ setAccessControlConditions }: Access setTokenId(Number(e.target.value)) }} /> - {errors.tokenId &&

    {String(errors.tokenId?.message)}

    } + {errors.tokenId && ( +

    + {String(errors.tokenId?.message)} +

    + )}
    - +
    ) } -const getAccessControlConditions = (chain: string, address: string, tokenId: string) => { +const getAccessControlConditions = ( + chain: string, + address: string, + tokenId: string +) => { return [ { - conditionType: 'evmBasic', + conditionType: "evmBasic", contractAddress: address, - standardContractType: 'ERC721', + standardContractType: "ERC721", chain, - method: 'ownerOf', + method: "ownerOf", parameters: [tokenId], returnValueTest: { - comparator: '=', - value: ':userAddress', + comparator: "=", + value: ":userAddress", }, }, ] diff --git a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-token-group.tsx b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-token-group.tsx index 65106a2..88934bd 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-token-group.tsx +++ b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/access-control-token-group.tsx @@ -1,12 +1,18 @@ -import { useState } from 'react' +import { useState } from "react" +import { useForm } from "react-hook-form" +import { isAddress } from "viem" -import { useForm } from 'react-hook-form' -import { isAddress } from 'viem' +import { Button } from "@/components/ui/button" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' - -import { AccessControlProps } from './types' -import { supportedChains } from '../../utils/config' +import { supportedChains } from "../../utils/config" +import { AccessControlProps } from "./types" interface FormSchema { chain: string @@ -17,13 +23,15 @@ interface FormSchema { tokenDecimals: string } -export function AccessControlTokenGroup({ setAccessControlConditions }: AccessControlProps) { - const [address, setAddress] = useState('') +export function AccessControlTokenGroup({ + setAccessControlConditions, +}: AccessControlProps) { + const [address, setAddress] = useState("") const [tokenAmount, setTokenAmount] = useState(0) const [tokenType, setTokenType] = useState() const [tokenDecimals, setTokenDecimals] = useState(18) const [tokenId, setTokenId] = useState(0) - const [chain, setChain] = useState('ethereum') + const [chain, setChain] = useState("ethereum") const { register, @@ -32,7 +40,16 @@ export function AccessControlTokenGroup({ setAccessControlConditions }: AccessCo } = useForm() const onSubmit = ({ address, tokenId, tokenAmount }: FormSchema) => { - setAccessControlConditions(getAccessControlConditions(chain, address, tokenType || '', tokenAmount, tokenId, tokenDecimals)) + setAccessControlConditions( + getAccessControlConditions( + chain, + address, + tokenType || "", + tokenAmount, + tokenId, + tokenDecimals + ) + ) } return ( @@ -40,11 +57,15 @@ export function AccessControlTokenGroup({ setAccessControlConditions }: AccessCo
    - setChain(value)} + > + - + {supportedChains.map((chain) => ( {chain} @@ -56,32 +77,39 @@ export function AccessControlTokenGroup({ setAccessControlConditions }: AccessCo
    - {errors.tokenType &&

    {String(errors.tokenType?.message)}

    } + {errors.tokenType && ( +

    + {String(errors.tokenType?.message)} +

    + )}
    isAddress(value) || 'Invalid Contract address', + isValidEthereumAddress: (value) => + isAddress(value) || "Invalid Contract address", }, })} placeholder="0x1234567890123456789012345678901234567890" @@ -89,16 +117,21 @@ export function AccessControlTokenGroup({ setAccessControlConditions }: AccessCo value={address} onChange={(e) => setAddress(e.target.value)} /> - {errors.address &&

    {String(errors.address?.message)}

    } + {errors.address && ( +

    + {String(errors.address?.message)} +

    + )}
    Number(value) > 0 || 'Token amount must be greater than 0', + {...register("tokenAmount", { + required: "Token amount is required", + validate: (value) => + Number(value) > 0 || "Token amount must be greater than 0", })} value={tokenAmount} onChange={(e) => { @@ -106,47 +139,61 @@ export function AccessControlTokenGroup({ setAccessControlConditions }: AccessCo setTokenAmount(Number(e.target.value)) }} /> - {errors.tokenAmount &&

    {String(errors.tokenAmount?.message)}

    } + {errors.tokenAmount && ( +

    + {String(errors.tokenAmount?.message)} +

    + )}
    - {tokenType === 'ERC1155' && ( + {tokenType === "ERC1155" && (
    Number(value) >= 0 || 'Token ID must be positive', + {...register("tokenId", { + required: "Token ID is required", + validate: (value) => + Number(value) >= 0 || "Token ID must be positive", })} value={tokenId} onChange={(e) => { setTokenId(Math.floor(Number(e.target.value))) }} /> - {errors.tokenId &&

    {String(errors.tokenId?.message)}

    } + {errors.tokenId && ( +

    + {String(errors.tokenId?.message)} +

    + )}
    )} - {tokenType === 'ERC20' && ( + {tokenType === "ERC20" && (
    Number(value) > 0 || 'Token decimals must be positive', + {...register("tokenDecimals", { + required: "ERC20 Decimals is required", + validate: (value) => + Number(value) > 0 || "Token decimals must be positive", })} value={tokenDecimals} onChange={(e) => { setTokenDecimals(Math.floor(Number(e.target.value))) }} /> - {errors.tokenDecimals &&

    {String(errors.tokenDecimals?.message)}

    } + {errors.tokenDecimals && ( +

    + {String(errors.tokenDecimals?.message)} +

    + )}
    )} - +
    ) @@ -162,15 +209,19 @@ const getAccessControlConditions = ( ) => { return [ { - conditionType: 'evmBasic', + conditionType: "evmBasic", contractAddress: address, standardContractType: tokenType, chain, - method: 'balanceOf', - parameters: tokenType === 'ERC1155' ? [':userAddress', tokenId] : [':userAddress'], + method: "balanceOf", + parameters: + tokenType === "ERC1155" ? [":userAddress", tokenId] : [":userAddress"], returnValueTest: { - comparator: '>=', - value: tokenType === 'ERC20' ? String(Number(tokenAmount) * 10 ** tokenDecimals) : tokenAmount, + comparator: ">=", + value: + tokenType === "ERC20" + ? String(Number(tokenAmount) * 10 ** tokenDecimals) + : tokenAmount, }, }, ] diff --git a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/index.ts b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/index.ts index fdc2772..15982ff 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/index.ts +++ b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/index.ts @@ -1,5 +1,9 @@ -import { AccessControlSingleAddress } from './access-control-single-address' -import { AccessControlSingleERC721 } from './access-control-single-erc721' -import { AccessControlTokenGroup } from './access-control-token-group' +import { AccessControlSingleAddress } from "./access-control-single-address" +import { AccessControlSingleERC721 } from "./access-control-single-erc721" +import { AccessControlTokenGroup } from "./access-control-token-group" -export { AccessControlSingleAddress, AccessControlSingleERC721, AccessControlTokenGroup } +export { + AccessControlSingleAddress, + AccessControlSingleERC721, + AccessControlTokenGroup, +} diff --git a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/types.ts b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/types.ts index 948729c..5156ab3 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/components/access-control/types.ts +++ b/template/integrations/lit-protocol/core/lit-protocol/components/access-control/types.ts @@ -1,6 +1,6 @@ -import { Dispatch, SetStateAction } from 'react' +import { Dispatch, SetStateAction } from "react" -import { AccessControlConditions } from '../../utils/types' +import { AccessControlConditions } from "../../utils/types" export interface AccessControlProps { accessControlConditions: AccessControlConditions diff --git a/template/integrations/lit-protocol/core/lit-protocol/components/form-lit-decrypt-message.tsx b/template/integrations/lit-protocol/core/lit-protocol/components/form-lit-decrypt-message.tsx index 7f7a69b..6d864cc 100644 --- a/template/integrations/lit-protocol/core/lit-protocol/components/form-lit-decrypt-message.tsx +++ b/template/integrations/lit-protocol/core/lit-protocol/components/form-lit-decrypt-message.tsx @@ -1,23 +1,29 @@ -import { useState } from 'react' +import { useState } from "react" +import { Separator } from "@radix-ui/react-select" +import { motion } from "framer-motion" +import { useForm } from "react-hook-form" -import { motion } from 'framer-motion' -import { useForm } from 'react-hook-form' +import { FADE_DOWN_ANIMATION_VARIANTS } from "@/config/design" +import { useToast } from "@/lib/hooks/use-toast" +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardFooter } from "@/components/ui/card" +import { Textarea } from "@/components/ui/textarea" +import { WalletConnect } from "@/components/blockchain/wallet-connect" +import { IsWalletConnected } from "@/components/shared/is-wallet-connected" +import { IsWalletDisconnected } from "@/components/shared/is-wallet-disconnected" -import { WalletConnect } from '@/components/blockchain/wallet-connect' -import { IsWalletConnected } from '@/components/shared/is-wallet-connected' -import { IsWalletDisconnected } from '@/components/shared/is-wallet-disconnected' -import { Textarea } from '@/components/ui/textarea' -import { FADE_DOWN_ANIMATION_VARIANTS } from '@/config/design' -import { useToast } from '@/lib/hooks/use-toast' - -import { useLitClient } from '../hooks/use-lit-client' +import { useLitClient } from "../hooks/use-lit-client" interface FormLitDecryptMessageProps { initialEencryptedMessageId: string } -export function FormLitDecryptMessage({ initialEencryptedMessageId }: FormLitDecryptMessageProps) { - const [encryptedMessageId, setEncryptedMessageId] = useState(initialEencryptedMessageId) +export function FormLitDecryptMessage({ + initialEencryptedMessageId, +}: FormLitDecryptMessageProps) { + const [encryptedMessageId, setEncryptedMessageId] = useState( + initialEencryptedMessageId + ) const [decryptedMessage, setDecryptedMessage] = useState() const [isLoading, setIsLoading] = useState(false) @@ -35,14 +41,20 @@ export function FormLitDecryptMessage({ initialEencryptedMessageId }: FormLitDec if (!error) { setDecryptedMessage(decryptedString) - } else if (error === 'Message not found') { - handleToast('No ID found', 'The ID you entered does not match any encrypted message. Please check the ID and try again.') + } else if (error === "Message not found") { + handleToast( + "No ID found", + "The ID you entered does not match any encrypted message. Please check the ID and try again." + ) return - } else if (error === 'Access denied') { - handleToast('Access denied', 'Your address do not met the access control conditions of this message.') + } else if (error === "Access denied") { + handleToast( + "Access denied", + "Your address do not met the access control conditions of this message." + ) return } else { - handleToast('Error', 'Something went wrong. Please try again.') + handleToast("Error", "Something went wrong. Please try again.") return } } @@ -64,35 +76,63 @@ export function FormLitDecryptMessage({ initialEencryptedMessageId }: FormLitDec
    - - setEncryptedMessageId(e.target.value)} - /> - -
    -
    -

    Encrypted message ID

    -

    The ID of the encrypted message saved into a database.

    -
    + onSubmit={handleSubmit(onSubmit)} + > + + + + setEncryptedMessageId(e.target.value)} + /> + + + + +

    Encrypted message ID

    +

    + The ID of the encrypted message saved into a database. +

    +
    +
    {decryptedMessage && ( - -

    Decrypted Message:

    -