Compare commits

...
Author SHA1 Message Date
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
22e066c611 Version Packages (#4514)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-13 06:10:23 +12:00
MananTank 0abd8f462c Add Modular contracts banner on dashboard and portal (#4560)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to introduce a new `AnnouncementBanner` component in the dashboard and portal pages.

### Detailed summary
- Added `AnnouncementBanner` component to display announcements in dashboard and portal
- Updated announcement content and link in `AnnouncementBanner`
- Updated `Banner` component in portal
- Reorganized imports in `layout.tsx` and made minor changes in layout components

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 16:53:08 +00:00
gregfromstl 5fe9476475 [React] Fix: Linked account avatar sizing (#4559)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on updating the styling of the `LinkedProfilesScreen` component by changing the width property to flexGrow for better responsiveness.

### Detailed summary
- Updated `width` property to `flexGrow: 1` for responsiveness.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 16:40:11 +00:00
MananTank 5385f6bde1 Add Account Settings page (#4558)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates account and team-related UI components and settings pages.

### Detailed summary
- Updated titles in account and team UI components
- Added confirmation dialog to setting cards
- Improved layout in account and team pages
- Implemented account authentication logic

> The following files were skipped due to too many changes: `apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/GeneralSettingsPage.tsx`, `apps/dashboard/src/app/account/settings/AccountSettingsPageUI.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 16:34:41 +00:00
arcoraven f092b2aeac chore: allow disabling a chain's faucet (#4556)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to add functionality to disable the faucet for specific chain IDs in the testnet-faucet API route.

### Detailed summary
- Added `DISABLE_FAUCET_CHAIN_IDS` environment variable for comma-separated list of chain IDs to disable the faucet.
- Implemented logic to check if the faucet is disabled for a specific chain ID before allowing claims.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 11:25:59 +00:00
joaquim-verges df42167f94 fix: deterministic direct deploys (#4555)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates contract deployment logic to include salt and adds a check for contract deployment status in tests.

### Detailed summary
- Updated contract deployment to include salt for CREATE2 deployment
- Added check for contract deployment status in tests using `isContractDeployed` function

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 10:16:25 +00:00
jnsdls edce1bf2f7 [Update] Enhance function selector resolution with ABI support (#4554) 2024-09-12 09:59:26 +00:00
joaquim-verges 1f7bf82fe0 docs: modular contract docs (#4553)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates JSDoc annotations in various files to change `@module` to `@modules` for improved clarity and consistency.

### Detailed summary
- Updated JSDoc annotations from `@module` to `@modules` in multiple files
- Added `@social` tag in `tsdoc.json`
- Updated annotations in ERC20, ERC721, and ERC1155 related modules

> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/airdrop/write/merkleInfoERC721.ts`, `packages/thirdweb/src/extensions/airdrop/write/merkleInfoERC1155.ts`, `packages/thirdweb/src/extensions/modules/common/installPublishedModule.ts`, `packages/thirdweb/src/extensions/modules/common/uninstallPublishedModule.ts`, `packages/thirdweb/src/extensions/modules/MintableERC20/mintWithSignature.ts`, `packages/thirdweb/src/extensions/modules/MintableERC721/mintWithSignature.ts`, `packages/thirdweb/src/extensions/modules/MintableERC1155/mintWithSignature.ts`, `packages/thirdweb/src/extensions/split/read/getRecipientSplitPercentage.ts`, `packages/thirdweb/src/react/core/utils/defaultTokens.ts`, `apps/portal/src/app/typescript/v5/modular/upgrade/page.mdx`, `packages/thirdweb/src/extensions/airdrop/write/airdropERC20WithSignature.ts`, `packages/thirdweb/src/extensions/airdrop/write/airdropERC721WithSignature.ts`, `packages/thirdweb/src/extensions/airdrop/write/airdropERC1155WithSignature.ts`, `packages/thirdweb/src/extensions/erc20/drops/read/getActiveClaimCondition.ts`, `packages/thirdweb/src/extensions/modules/BatchMetadataERC721/uploadMetadata.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC20/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC721/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC721/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC1155/module/install.ts`, `packages/thirdweb/src/extensions/modules/ClaimableERC20/setClaimConditions.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC20/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC1155/module/install.ts`, `packages/thirdweb/src/extensions/modules/ClaimableERC721/setClaimConditions.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC20/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC721/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC721/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC1155/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC1155/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/SequentialTokenIdERC1155/module/install.ts`, `packages/thirdweb/src/extensions/modules/__generated__/OpenEditionMetadataERC721/module/install.ts`, `packages/thirdweb/src/extensions/modules/ClaimableERC1155/setClaimConditions.ts`, `apps/portal/src/app/typescript/v5/modular/interact/page.mdx`, `apps/portal/src/app/typescript/v5/modular/deploy/page.mdx`, `packages/thirdweb/src/extensions/modules/BatchMetadataERC1155/uploadMetadata.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC721Core/read/totalMinted.ts`, `packages/thirdweb/scripts/generate/generate.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC20/read/getSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC20/read/getSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC721/read/getSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC721/read/getSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC1155/read/getSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC1155/read/getSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC20/read/getClaimCondition.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC721/read/getClaimCondition.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/read/getTransferValidator.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/read/getDefaultRoyaltyInfo.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/read/getTransferValidator.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/read/getDefaultRoyaltyInfo.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC20/read/isTransferEnabled.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC721/read/isTransferEnabled.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/read/getTransferValidationFunction.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC1155/read/isTransferEnabled.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC20/read/encodeBytesOnInstall.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/read/getTransferValidationFunction.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC721/read/encodeBytesOnInstall.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC721/read/encodeBytesOnInstall.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC1155/read/encodeBytesOnInstall.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC721/read/getAllMetadataBatches.ts`, `packages/thirdweb/src/extensions/modules/__generated__/SequentialTokenIdERC1155/read/getNextTokenId.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC1155/read/encodeBytesOnInstall.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC1155/read/getAllMetadataBatches.ts`, `packages/thirdweb/src/extensions/modules/__generated__/SequentialTokenIdERC1155/read/encodeBytesOnInstall.ts`, `packages/thirdweb/src/extensions/modules/__generated__/OpenEditionMetadataERC721/read/encodeBytesOnInstall.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC20Core/write/burn.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC20Core/write/mint.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC721Core/write/burn.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC721Core/write/mint.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC1155Core/write/burn.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC1155Core/write/mint.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC20Core/write/initialize.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC721Core/write/initialize.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC1155Core/write/initialize.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC20Core/write/mintWithSignature.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC721Core/write/mintWithSignature.ts`, `packages/thirdweb/src/extensions/modules/common/getOrDeployModule.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ERC1155Core/write/mintWithSignature.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC1155/write/setTokenURI.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC20/write/setSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/write/setRoyaltyInfoForToken.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC20/write/setSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC721/write/setSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/write/setTransferValidator.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/write/setRoyaltyInfoForToken.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC721/write/setSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/MintableERC1155/write/setSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/write/setTransferValidator.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/write/setDefaultRoyaltyInfo.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC20/write/setClaimCondition.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC1155/write/setSaleConfig.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/write/setDefaultRoyaltyInfo.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC721/write/setClaimCondition.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC20/write/setTransferable.ts`, `apps/portal/src/app/typescript/v5/modular/page.mdx`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC20/write/setTransferableFor.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC721/write/uploadMetadata.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC721/write/setTransferable.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC721/write/setTransferableFor.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC1155/write/setClaimConditionByTokenId.ts`, `packages/thirdweb/src/extensions/modules/__generated__/BatchMetadataERC1155/write/uploadMetadata.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC1155/write/setTransferable.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC1155/write/setTransferableFor.ts`, `packages/thirdweb/src/extensions/modules/__generated__/OpenEditionMetadataERC721/write/setSharedMetadata.ts`, `apps/portal/src/app/typescript/v5/modular/built-in/page.tsx`, `apps/portal/src/app/typescript/v5/extensions/built-in/page.tsx`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/read/royaltyInfo.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/read/royaltyInfo.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC721/read/getRoyaltyInfoForToken.ts`, `packages/thirdweb/src/extensions/modules/__generated__/RoyaltyERC1155/read/getRoyaltyInfoForToken.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC20/read/isTransferEnabledFor.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC721/read/isTransferEnabledFor.ts`, `packages/thirdweb/src/extensions/modules/__generated__/TransferableERC1155/read/isTransferEnabledFor.ts`, `packages/thirdweb/src/extensions/modules/__generated__/ClaimableERC1155/read/getClaimConditionByTokenId.ts`, `packages/thirdweb/src/extensions/modules/__generated__/OpenEditionMetadataERC721/read/onTokenURI.ts`, `packages/thirdweb/src/exports/modules.ts`, `apps/portal/src/app/references/components/TDoc/utils/slugs.ts`, `apps/portal/src/app/references/components/TDoc/utils/getSidebarLinkgroups.ts`, `apps/portal/src/app/typescript/v5/sidebar.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 09:41:36 +00:00
kien-ngo 28c46b3bc4 Update list-form.tsx (#4551)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the `ListForm` interface and adjust form inputs for creating direct listings and auctions.

### Detailed summary
- Refactored `ListForm` interface for direct listings and auctions
- Adjusted form inputs for price per token in direct listings and auctions

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 08:22:26 +00:00
joaquim-verges 40f01e5e89 feat: allow sending smart wallet transactions in parallel (#4544)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enabling smart wallet transactions to be sent in parallel.

### Detailed summary
- Added the ability to execute 2 transactions in parallel in smart wallets
- Implemented functions to handle account deployment in parallel transactions

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 08:06:33 +00:00
kien-ngo 6077fedd75 [Dashboard] Chain page breadcrumb (#4525)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a new "Add Your Chain" button, implement breadcrumb UI components, and enhance chain filtering options in the dashboard.

### Detailed summary
- Added `AddYourChainButton` component with a link to add a chain
- Implemented breadcrumb UI components (`Breadcrumb`, `BreadcrumbItem`, etc.)
- Enhanced chain filtering options in the dashboard with `AllFilters` and related components

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/(chainPage)/layout.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/chainlist/components/server/chain-table.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/chainlist/page.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 05:56:41 +00:00
saminaandGitHub 17e041642f Update page.mdx (#4547)
Signed-off-by: samina <[email protected]>
2024-09-11 22:55:46 -07:00
Jonas DanielsandGitHub 05866381b4 [Update] Improve IP address detection for testnet faucet (#4549) 2024-09-11 22:44:59 -07:00
Jonas DanielsandGitHub aabce93330 [Update] Enhance testnet faucet rate limiting and error handling (#4548) 2024-09-11 22:14:06 -07:00
gregfromstl b4e149101d [React] Feature: Show social profiles on linked accounts (#4543)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds functionality to display social profiles for linked accounts in the `LinkedProfilesScreen`.

### Detailed summary
- Added a new `LinkedProfile` component to display social profiles
- Used `useSocialProfiles` hook to fetch social profiles data
- Updated rendering logic for linked profiles
- Improved UI for displaying linked profiles

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 01:07:28 +00:00
gregfromstl 0596fa24c2 [SDK] Feature: adds findUsers function (#4541)
This replaces https://portal.thirdweb.com/connect/in-app-wallet/how-to/get-in-app-wallet-details-on-server

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a `getUser` function to query users on the server and updates related files with necessary exports and type definitions.

### Detailed summary
- Added `getUser` function to query users on the server
- Updated exports in `thirdweb.ts` and `wallets.ts`
- Added type definitions for `GetUserResult`
- Implemented `getUser` function in `getUser.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 00:51:32 +00:00
GWSzeto eda37f3031 updated documentation (#4526)
Updates the documentation of the modular contracts based on the recent refactor

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update metadata and add GitHub links to modular contract pages.

### Detailed summary
- Added GitHub links to `TransferableERC20`, `TransferableERC721`, `TransferableERC1155`, and `SequentialTokenIdERC1155` contracts
- Updated metadata for contract pages
- Removed unnecessary image imports
- Updated sidebar links for `Get Started` section in the contracts page

> The following files were skipped due to too many changes: `apps/portal/src/app/contracts/sidebar.tsx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-20/misc/creatorTokenERC20/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-1155/metadata/openEditionMetadataERC1155/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-721/metadata/openEditionMetadataERC721/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/core-contracts/erc-721/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-20/minting/mintableERC20/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-721/minting/mintableERC721/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-1155/minting/mintableERC1155/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/core-contracts/erc-20/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/core-contracts/erc-1155/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-1155/metadata/batchMetadataERC1155/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-721/metadata/batchMetadataERC721/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-1155/minting/claimableERC1155/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/get-started/deploy-modular-contract/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-20/minting/claimableERC20/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-721/minting/claimableERC721/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/get-started/create-core-contract/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-721/misc/royaltyERC721/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/module-contracts/erc-1155/misc/royaltyERC1155/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/how-it-works/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/get-started/page.mdx`, `apps/portal/src/app/contracts/modular-contracts/get-started/create-module-contract/page.mdx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-12 00:47:29 +00:00
MananTank c3800d6c07 Add Linked wallets page in account layout (#4542)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates account pages with new components, refactors UI, and adds wallet linking functionality.

### Detailed summary
- Updated components for account teams and wallets
- Added wallet linking feature
- Refactored UI for better user experience

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 23:01:52 +00:00
ianandGitHub adbac73065 Ian/multi auth (#4538) 2024-09-11 14:42:52 -07:00
Joe-ThirdwebandGitHub 4f2c15f3cd Joe OP Superchain Additions. (#4532) 2024-09-11 14:27:27 -07:00
Giselle Chacon NessiandGitHub af13b5600d Fix gradient overlap on apply button - startup page (#4535) 2024-09-11 13:55:45 -07:00
MananTank 6991ac57f6 Add Account overview page (#4539)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `TeamPlanBadge` component and updates various files to use it for displaying team plans.

### Detailed summary
- Added `TeamPlanBadge` component to display team plans
- Updated `TeamHeaderUI` and `AccountTeamsUI` components to use `TeamPlanBadge`
- Created stories for `AccountTeamsUI` component with different team plan variants

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 20:32:27 +00:00
MananTank bd3e04d717 Fix Progress bar overflow on UsageCard (#4540)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR improves the UsageCard component in the Account settings by ensuring the width does not exceed 100%.

### Detailed summary
- Updated the width calculation in the UsageCard component to ensure it does not exceed 100%

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 20:27:24 +00:00
MananTank 723137820f Fix SidebarLayout overflow and billing alert spacing (#4537)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update the styling of alerts in the `Alert.tsx` component and add `overflow-hidden` to the main content in `SidebarLayout.tsx`.

### Detailed summary
- Updated styling of alerts in `Alert.tsx` by replacing `mb={12}` with `className="py-6"`
- Added `overflow-hidden` to the main content in `SidebarLayout.tsx` for better layout.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 18:40:27 +00:00
gregfromstl 7bad5ef1ad [Playground] Feature: Update social api page (#4529)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the `SocialProfiles` component, update dependencies, and improve profile card rendering.

### Detailed summary
- Refactored `SocialProfiles` component to use `useMutation` and `useState`
- Updated dependencies for `@react-native-async-storage/async-storage`
- Improved profile card rendering with `ProfileCard` component
- Added error handling for weird avatar formats in `SocialProfileCard`

> The following files were skipped due to too many changes: `pnpm-lock.yaml`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 16:43:42 +00:00
MananTank 52b2ec23e2 Sidebars cleanup (#4534)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates sidebar components and layouts in the dashboard, adds `SidebarLayout`, and refactors sidebar imports.

### Detailed summary
- Updated sidebar links in various components
- Added `SidebarLayout` component
- Refactored sidebar imports to use `SidebarLayout` and `MobileSidebar`
- Updated layout components for better organization

> The following files were skipped due to too many changes: `apps/dashboard/src/pages/dashboard/settings/notifications.tsx`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/create/EcosystemCreatePage.tsx`, `apps/dashboard/src/pages/dashboard/settings/api-keys/[id].tsx`, `apps/dashboard/src/pages/dashboard/settings/api-keys/index.tsx`, `apps/dashboard/src/pages/dashboard/contracts/publish.tsx`, `apps/dashboard/src/components/layout/app-shell/index.tsx`, `apps/dashboard/src/pages/dashboard/settings/devices.tsx`, `apps/dashboard/src/pages/publish/[...paths].tsx`, `apps/dashboard/src/pages/dashboard/contracts/deploy.tsx`, `apps/dashboard/src/@/components/blocks/SidebarLayout.stories.tsx`, `apps/dashboard/.storybook/preview.tsx`, `apps/dashboard/src/pages/dashboard/connect/analytics.tsx`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/layout.tsx`, `apps/dashboard/src/pages/dashboard/connect/in-app-wallets.tsx`, `apps/dashboard/src/pages/dashboard/settings/storage.tsx`, `apps/dashboard/src/app/(dashboard)/trending/page.tsx`, `apps/dashboard/src/pages/dashboard/connect/account-abstraction.tsx`, `apps/dashboard/src/app/account/layout.tsx`, `apps/dashboard/src/@/components/blocks/MobileSidebar.tsx`, `apps/dashboard/src/@/components/blocks/Sidebar.tsx`, `apps/dashboard/src/core-ui/sidebar/contracts.tsx`, `apps/dashboard/src/pages/explore/index.tsx`, `apps/dashboard/src/core-ui/sidebar/settings.tsx`, `apps/dashboard/src/components/pages/publish.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 16:15:23 +00:00
kien-ngo d6e9def577 [Dashboard] Fix url params for fn overload (#4324)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR exposes `toFunctionSelector` and updates its usage in the dashboard component for better navigation.

### Detailed summary
- Exposed `toFunctionSelector` in `thirdweb/utils.ts`
- Updated `ContractFunctionsPanel` to use `toFunctionSelector` for navigation based on function selector
- Improved function selection logic in `FunctionsOrEventsListItem` to use `toFunctionSelector`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 16:07:45 +00:00
joaquim-verges 75855ea345 feat: Add examples for contract deployment and export new functions (#4533)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding new types and functions related to deploy transactions and contracts in the `thirdweb` package.

### Detailed summary
- Added `DeployTransactionType` and `DeployTransactionResult` types
- Added `getRequiredTransactionCount` function
- Updated exports for deploy functions
- Updated examples and comments for deploying contracts

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 09:31:34 +00:00
Joaquim VergesandGitHub b76a82c303 Update React Native dependencies and add support for React Native 0.75 (#4527) 2024-09-11 20:37:58 +12:00
gregfromstl 25ad74fb55 [Portal] Docs: Adds Social API docs (#4531)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds `Social API` functionality to the codebase.

### Detailed summary
- Added `@social` tag to `useSocialProfiles` and `getSocialProfiles` functions
- Updated sidebar components to include `Social API` section with relevant links
- Updated sidebar order to include `Social API` in the correct position

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 06:43:06 +00:00
kien-ngo 20a74eb0f5 [Dashboard] Fix BigInt serialize issue (#4530)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates NFT handling in the wallet and contract UI components. It replaces `BigInt` with `string` for `id` and `supply` fields to prevent serialization errors and improves metadata handling.

### Detailed summary
- Replaced `BigInt` with `string` for `id` and `supply` fields in `WalletNFT` type
- Updated NFT handling in various files to ensure compatibility and prevent serialization errors

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 06:22:07 +00:00
joaquim-verges c96e2d9777 feat: handle salt for deterministic deploys (#4528)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding support for deterministic contract deployment using a salt parameter.

### Detailed summary
- Added support for passing a `salt` parameter for deterministic contract deployment
- Updated functions to handle the `salt` parameter for deterministic deploys
- Modified tests to include deterministic deployment scenarios

> The following files were skipped due to too many changes: `packages/thirdweb/src/extensions/prebuilts/deploy-published.ts`, `packages/thirdweb/src/contract/deployment/deploy-with-abi.ts`, `packages/thirdweb/test/src/abis/proxy.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 05:16:55 +00:00
jnsdls beb894a355 Matic -> POL (#4524) 2024-09-11 02:35:36 +00:00
jnsdls 495aa042ba ts 5.6 and other updates (#4522)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to update dependencies across multiple packages and fix minor code issues.

### Detailed summary
- Updated `typedoc` to versions `0.26.7`.
- Updated `tsup` to versions `8.2.4`.
- Updated `typescript` to versions `5.6.2`.
- Updated various packages to use the latest TypeScript version.
- Fixed minor code issues like handling `delete` operations and eslint comments.

> The following files were skipped due to too many changes: `packages/thirdweb/src/wallets/in-app/core/eip5972/in-app-wallet-calls.ts`, `legacy_packages/wallets/src/evm/connectors/gcp-kms/utils/gcp-kms-utils.ts`, `apps/dashboard/package.json`, `packages/thirdweb/package.json`, `legacy_packages/contracts-js/package.json`, `pnpm-lock.yaml`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 02:35:34 +00:00
kien-ngo 02aabbf97d [SDK] More tests for NFT extensions (#4470)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to add tests and functions for updating token URIs in ERC721 and ERC1155 contracts.

### Detailed summary
- Added test for updating metadata in ERC721 contract
- Added test for updating metadata in ERC1155 contract
- Created functions to update token URIs in ERC721 and ERC1155 contracts

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 02:09:30 +00:00
MananTank 4762ca4dd9 Add initial Account layout (#4517)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates file imports, restructures components, and adds a new `TabPathLinks` component.

### Detailed summary
- Updated file imports for components
- Reorganized component structure
- Added `TabPathLinks` component
- Updated `AccountHeader` component for accounts
- Added `HeaderAndNav` function for account layout

> The following files were skipped due to too many changes: `apps/dashboard/src/stories/stubs.ts`, `apps/dashboard/src/app/account/components/AccountHeaderUI.stories.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/layout.tsx`, `apps/dashboard/src/app/components/Header/TeamHeader/TeamHeaderUI.tsx`, `apps/dashboard/src/app/components/Header/TeamHeader/TeamSelectionUI.tsx`, `apps/dashboard/src/app/account/components/AccountHeaderUI.tsx`, `apps/dashboard/src/app/components/Header/TeamHeader/TeamHeaderUI.stories.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 01:48:24 +00:00
joaquim-verges fe1ff63bf9 Get transaction count for deploys (#4499)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating contract initializer parameters from arrays to objects for better clarity and consistency.

### Detailed summary
- Updated contract initializer params from arrays to objects for improved readability
- Refactored multiple files to use object-based contract params
- Added normalization function for function parameters in zkDeployContract file

> The following files were skipped due to too many changes: `packages/thirdweb/src/contract/deployment/zksync/zkDeployContract.ts`, `packages/thirdweb/src/utils/abi/normalizeFunctionParams.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-published.test.ts`, `packages/thirdweb/test/src/chains.ts`, `packages/thirdweb/src/contract/deployment/zksync/zkDeployDeterministic.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-marketplace.ts`, `packages/thirdweb/src/utils/any-evm/compute-published-contract-address.ts`, `packages/thirdweb/src/utils/any-evm/compute-published-contract-deploy-info.ts`, `packages/thirdweb/src/contract/deployment/deploy-deterministic.test.ts`, `packages/thirdweb/src/extensions/prebuilts/get-required-transactions.test.ts`, `packages/thirdweb/src/extensions/prebuilts/get-required-transactions.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-modular-core.test.ts`, `packages/thirdweb/src/contract/deployment/utils/infra.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-modular.ts`, `packages/thirdweb/src/contract/deployment/utils/bootstrap.ts`, `packages/thirdweb/src/extensions/prebuilts/deploy-published.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 01:16:26 +00:00
kien-ngo bcd033b0ef [SDK] test for ERC20 transferFrom (#4471)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding tests for ERC20 token `transferFrom` function with different `amount` and `amountWei` parameters.

### Detailed summary
- Added tests for `transferFrom` function with `amount` parameter
- Added tests for `transferFrom` function with `amountWei` parameter
- Utilized test utilities and prebuilt functions
- Checked balance after transfer for correctness

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 00:50:24 +00:00
kien-ngo 052f56a433 [SDK] Add test getApprovalForTransaction.test.ts (#4472)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add test cases for the `getApprovalForTransaction` function in the ERC20 extension.

### Detailed summary
- Added test cases for `getApprovalForTransaction` function
- Imported necessary modules for testing
- Defined test parameters such as chain, client, and accounts
- Tested approval transaction scenarios

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-11 00:20:33 +00:00
gregfromstl 2166dec9cb [SDK] Feature: Pay analytics (#4490)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding tracking events for various payment methods and transactions across different screens.

### Detailed summary
- Added a new function `trackPayEvent` for tracking payment events
- Added tracking events for different payment methods like buy, direct payment, and swap
- Updated multiple files to include tracking events for specific user actions

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 23:44:23 +00:00
jnsdls 865c5e3b16 [Update] Handle undefined wallet address in WalletAddress component (#4523)
### TL;DR

Enhanced the WalletAddress component to handle undefined addresses and improved address display.

### What changed?

- Added support for undefined addresses by defaulting to ZERO_ADDRESS
- Updated address processing to use the new default address
- Made the WalletAvatar conditional on the presence of an address
- Imported ZERO_ADDRESS from the thirdweb library

### How to test?

1. Render the WalletAddress component with an undefined address
2. Verify that it displays the ZERO_ADDRESS
3. Test with valid addresses to ensure proper shortening and display
4. Check that the WalletAvatar only appears when an address is provided

### Why make this change?

This change improves the robustness of the WalletAddress component by gracefully handling undefined addresses. It prevents potential errors and provides a consistent display even when address data is missing or not yet loaded.

---
2024-09-10 23:24:26 +00:00
jnsdls 5d77096129 [Refactor] Replace StorageSingleton with thirdweb storage download (#4518)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates NFT transformation functions in `alchemy.ts` and `moralis.ts` to use `download` method with `thirdwebClient` for fetching metadata.

### Detailed summary
- Updated NFT transformation functions to use `download` method with `thirdwebClient` for fetching metadata
- Replaced `StorageSingleton.downloadJSON` with `download` method in both files

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 23:07:22 +00:00
gregfromstl ba3c8d0ea9 [Wallet UI] Fix: Redirect on login (#4521)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the login process by simplifying the `login` function and updating the login logic in the login page component.

### Detailed summary
- Simplified `login` function in `auth.ts`
- Updated login logic in `login/page.tsx` to use the simplified `login` function and navigate to wallet page after successful login

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 23:03:23 +00:00
Sergiy DybskiyandGitHub 5c4b1ae6a6 Update page.mdx (#4496)
Signed-off-by: Sergiy Dybskiy <[email protected]>
2024-09-10 15:56:54 -07:00
gregfromstl a1d0792f58 [Playground] Feature: Social Page (#4519)
<img width="1143" alt="Screenshot 2024-09-10 at 2 30 34 PM" src="https://github.com/user-attachments/assets/c3848dce-0bb2-4bd7-ac8a-300f1757a207">

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add social profile functionality and API documentation to the app.

### Detailed summary
- Added "Social" link to nav bar
- Implemented `Badge` component with variants
- Created `SocialProfiles` component to display user profiles
- Added API documentation for social profiles

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 22:35:02 +00:00
gregfromstl f914379421 [SDK] Feature: Adds LINE auth method (#4500)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add support for LINE authentication in various parts of the application.

### Detailed summary
- Added LINE authentication option in multiple files
- Updated constants and icons for LINE
- Integrated LINE in authentication flows

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 21:33:22 +00:00
Jonas DanielsandGitHub 8cc1d36a9e [Update] Remove console log and optimize NFT reveal check (#4516) 2024-09-10 13:15:50 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8a58ca7639 Bump @walletconnect/ethereum-provider from 2.15.2 to 2.16.1 (#4486)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-10 13:15:14 -07:00
jnsdls 9aa5b621ed wallet social profiles in dashboard (#4502)
before:
<img width="1942" alt="image" src="https://github.com/user-attachments/assets/8ac59faa-f81c-4670-98e5-f750ce68e5d6">

after:
<img width="1819" alt="image" src="https://github.com/user-attachments/assets/c8c015eb-528b-44a7-8cd5-478ca3ca907e">

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds social API domain support, new UI components, and updates dependencies.

### Detailed summary
- Added `THIRDWEB_SOCIAL_API_DOMAIN` in `urls.ts`
- Added `WalletAddress` component
- Updated UI components in various files
- Added new dependencies like `@radix-ui/react-avatar` and `@radix-ui/react-hover-card`

> The following files were skipped due to too many changes: `apps/dashboard/src/components/wallets/ConnectWalletMiniPlayground/MiniPlayground.tsx`, `apps/dashboard/src/contract-ui/tabs/analytics/page.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/split-fieldset.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx`, `apps/dashboard/src/contract-ui/tabs/manage/components/InstalledModulesTable.tsx`, `apps/dashboard/src/components/contract-components/contract-table-v2/index.tsx`, `apps/dashboard/src/components/pay/PayAnalytics/components/PaymentHistory.tsx`, `apps/dashboard/src/contract-ui/tabs/overview/components/MarketplaceDetails.tsx`, `apps/dashboard/src/contract-ui/tabs/nfts/components/table.tsx`, `apps/dashboard/src/pages/[chain_id]/[...paths].tsx`, `apps/dashboard/src/components/embedded-wallets/Users/index.tsx`, `apps/dashboard/src/components/engine/overview/engine-overview.tsx`, `apps/dashboard/src/components/engine/permissions/access-tokens-table.tsx`, `apps/dashboard/src/contract-ui/tabs/shared-components/marketplace-table.tsx`, `apps/dashboard/src/pages/dashboard/connect/analytics.tsx`, `apps/dashboard/src/app/(dashboard)/styleguide/page.tsx`, `apps/dashboard/src/contract-ui/tabs/account-permissions/components/account-signer.tsx`, `apps/dashboard/src/components/engine/engine-instances-table.tsx`, `apps/dashboard/src/components/engine/overview/backend-wallets-table.tsx`, `apps/dashboard/src/components/engine/overview/transactions-table.tsx`, `apps/dashboard/src/components/pay/PayAnalytics/components/PayCustomersTable.tsx`, `apps/dashboard/src/@/components/ui/hover-card.tsx`, `apps/dashboard/src/@/components/ui/avatar.tsx`, `apps/dashboard/src/@/components/blocks/wallet-address.tsx`, `pnpm-lock.yaml`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 19:52:29 +00:00
jnsdls 8636c28d0f [Update] Fix ERC721 delayed reveal detection and refactor related components (#4503)
FIX: DASH-218

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on fixing ERC721 delayed reveal detection and improving lazy minting functionality.

### Detailed summary
- Fixed ERC721 delayed reveal detection
- Updated functions related to lazy minting
- Improved detection of supported methods in ERC721 extensions

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 19:40:18 +00:00
Winston YeoandGitHub 50212762f3 docs: update query input and output type (#4509) 2024-09-11 07:39:02 +12:00
MananTank a1b17b9de3 Remove z-indices from ScrollShadow component (#4515)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes the `z-index` property from the `ScrollShadow` component in the dashboard UI to prevent stacking issues.

### Detailed summary
- Removed `z-index` property from `ScrollShadow` component in `ScrollShadow.tsx`
- Adjusted styles in `ScrollShadow.module.css` to remove `z-index` property

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 19:01:36 +00:00
MananTank 75b9131dc8 Add Account Abstraction pages in teams (#4510)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates API key handling and smart wallet components.

### Detailed summary
- Added `isOpChainId` function for chain validation
- Modified `getAuthToken` and `getAPIKey` functions
- Updated `SponsorshipPolicies` and `SmartWallets` components
- Introduced `AccountAbstractionPage` for account abstraction details

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 18:32:04 +00:00
gregfromstl 25654aed5f [Playground] Fix: No longer auto disconnect from modal (#4513)
Users are reporting the inability to login on the modal as a "bug" and it's causing confusion while trying to reproduce actual bugs. This PR allows connecting on the playground modal as is the expected behavior.

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update the `navLinks.ts` file and refactor the `RightSection.tsx` component in the `playground-web` app.

### Detailed summary
- Updated the name from "in-App wallet" to "In-App wallet" in `navLinks.ts`
- Refactored the `RightSection` component in `RightSection.tsx`
- Removed the `useDisconnect` hook and refactored the disconnect logic in `RightSection.tsx`
- Improved conditional rendering for the modal in `RightSection.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 18:24:30 +00:00
joaquim-verges 6353cb7331 test: Add createAndReveal test for ERC721 lazy minting (#4507)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR fixes an error related to ERC721 delayed reveal simulation.

### Detailed summary
- Fixed error in ERC721 delayed reveal simulation
- Added tests for `reveal` function
- Added `createAndReveal` function for batch creation and reveal

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 18:09:39 +00:00
MananTank 85facd88e4 Fix Get started with deploying contract card click, increase gap between steps (#4512)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to update the StepsCard component styling and add a new hook `useDashboardRouter` for navigation in the GetStartedWithContractsDeploy component.

### Detailed summary
- Updated styling in `StepsCard` component
- Added `useDashboardRouter` hook for navigation in `GetStartedWithContractsDeploy` component

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 17:41:40 +00:00
Manan TankandGitHub c321692e5c Fix ecosystem partners table (#4508) 2024-09-10 09:53:43 -07:00
jnsdls 0607d73340 FIX: DASH-217 (#4506) 2024-09-10 10:05:36 +00:00
jnsdls 7a4fadc38e [Update] Resolve image scheme in contract metadata (#4505)
<!-- start pr-codex -->

## PR-Codex overview
The focus of this PR is to update the `SettingsMetadata` component in `metadata.tsx` to resolve image URIs using the `resolveScheme` function from `thirdweb/storage`.

### Detailed summary
- Added import for `resolveScheme` from `thirdweb/storage`
- Resolved image URIs using `resolveScheme` function
- Updated the dependencies of the `useMemo` hook to include `contract.client`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 09:52:07 +00:00
kien-ngo ed39b3d5f5 [Portal] Update useProfiles docs (#4501)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the import in `useProfiles.ts` to use `useProfiles` instead of `use`.

### Detailed summary
- Updated import from `use` to `useProfiles` in `useProfiles.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 07:45:38 +00:00
Joe-ThirdwebandGitHub 9ddc625c37 Joe - SignIn + Pay redirects (#4485) 2024-09-10 00:44:19 -07:00
MananTank adf3404261 Fix deploy button link on contract page with search params in URL (#4495)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor imports in the `publish.tsx` file in the `dashboard` component.

### Detailed summary
- Added imports for `Badge`, `Button`, and `Select` components
- Updated imports for `usePathname` and `useSearchParams`
- Removed duplicate imports for `Select` related components
- Updated import paths for certain components

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 02:08:11 +00:00
gregfromstl eb88ed65a4 [Wallet UI] Fix: remove default favicon (#4494)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR removes the `favicon.ico` file from the `wallet-ui` app.

### Detailed summary
- Deleted the `favicon.ico` file from the `wallet-ui` app.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 01:57:35 +00:00
jnsdls 8119db08b3 v5 deploy sdk changes (#4497) 2024-09-10 00:33:26 +00:00
jnsdls bb9ce26d6b [Add] Support for modular contracts in explore and deploy pages (#4288)
<!-- start pr-codex -->

## PR-Codex overview
This PR updates contract components and SDK with default modules support.

### Detailed summary
- Added `defaultModules` support in SDK and contract components
- Updated contract card to display modules
- Added transfer validator field in royalty fieldset
- Improved contract publishing information display

> The following files were skipped due to too many changes: `apps/dashboard/src/components/contract-components/shared/published-by.tsx`, `apps/dashboard/src/components/contract-components/contract-deploy-form/custom-contract.tsx`, `apps/dashboard/src/components/explore/contract-card/index.tsx`, `apps/dashboard/src/components/pages/publish.tsx`, `apps/dashboard/src/data/explore.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-10 00:12:40 +00:00
MananTank 1ceaa72bef Add Ecosystem wallet pages in teams (#4491)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring ecosystem-related components and pages in the dashboard.

### Detailed summary
- Deleted `redirects.js`.
- Updated page components for ecosystem permissions and creation.
- Added new components for ecosystem layout and header.
- Modified functions for fetching ecosystem data and ecosystem selection.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/ecosystem-header.client.tsx`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/EcosystemLandingPage.tsx`, `apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/page.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 23:58:38 +00:00
MananTank efc12ee720 Remove sidebar on contracts page in teams layout (#4475)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to simplify the layout component in the contracts section by removing unnecessary elements and only rendering the children components.

### Detailed summary
- Removed sidebar and mobile sidebar components
- Simplified layout to only render children components

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 22:43:28 +00:00
MananTank f9953d36b1 Fix contact us form select element background (#4493)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a `data-theme` attribute to the `Card` component in `contact-us.tsx`.

### Detailed summary
- Added `data-theme="light"` attribute to the `Card` component.

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 22:37:42 +00:00
Jonas DanielsGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
85c951a2e6 Version Packages (#4453)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-09-10 09:55:54 +12:00
Joaquim VergesandGitHub e8bf4c54d0 chore: Remove trailing newline in assetchain-testnet.ts (#4492) 2024-09-10 09:38:01 +12:00
James Harrison SomtoandGitHub eb2d469c9e chores: added assetchain-testnet in chains folder (#4445) 2024-09-09 14:09:01 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bedac13b86 Bump @next/eslint-plugin-next from 14.2.8 to 14.2.9 (#4484)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 14:07:13 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
be76d991ff Bump knip from 5.29.2 to 5.30.1 (#4480)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-09 14:06:51 -07:00
gregandGitHub 80e0bfe4c5 [React SDK] Fix: Properly read current wallet erc20 balance (#4478) 2024-09-10 09:06:25 +12:00
Joaquim VergesandGitHub 371edea5be test: Conditionally run tests based on TW_SECRET_KEY (#4487) 2024-09-09 14:05:56 -07:00
gregandGitHub baf1a21d69 [SDK] Fix: SocialProfiles type changed to SocialProfile[] (#4473) 2024-09-10 09:05:39 +12:00
MananTank 4fcbe10ebc Migrate Notifications page to tailwind, Add in teams (#4474)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `SettingsNotificationsPage` component to handle notification settings in the dashboard.

### Detailed summary
- Added `SettingsNotificationsPage` component to manage notification settings
- Refactored `Page` component to use `SettingsNotificationsPage`
- Updated UI for notification settings with switches and labels

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 20:14:20 +00:00
kumaryash90 42feff3a3a More storage slots for proxy resolution (#4477)
## Problem solved

Add another storage slot for proxy resolution:

keccak256("org.zeppelinos.proxy.implementation")

ref: https://basescan.org/address/0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913#code

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to enhance proxy resolution in the `thirdweb` package by adding more storage slots for implementation addresses.

### Detailed summary
- Added more storage slots for proxy resolution in `resolveImplementation.test.ts`
- Updated global setup in `globalSetup` function in `globalSetup.ts`
- Added base chain definitions and RPC URL in `chains.ts`
- Improved implementation address extraction in `resolveImplementation.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 19:43:21 +00:00
arcoraven 7d508aa243 fix: add idempotency key to faucet (#4468)
Sets an idempotency key to Engine that prevents duplicate transactions _today_ (UTC).

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates error message spelling and adds idempotency key to prevent duplicate claims in the testnet faucet API.

### Detailed summary
- Fixed spelling error in error message
- Added idempotency key to prevent duplicate claims

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 08:35:13 +00:00
Joaquim VergesandGitHub 83de802c06 feat(docs): Add in-app wallet creation and authentication details (#4469) 2024-09-09 20:18:04 +12:00
Joaquim VergesandGitHub d8e7458b6c portal: Add Coinbase Wallet dedicated page on portal (#4467) 2024-09-09 17:33:06 +12:00
joaquim-verges 763e4390f7 chore: social named export (#4466)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to export additional social functions and types in the `thirdweb` package.

### Detailed summary
- Exported `SocialProfiles`, `EnsProfile`, `FarcasterProfile`, and `LensProfile` from `social/types.js`
- Added paths for social types in `package.json` under `social` section

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 04:36:29 +00:00
joaquim-verges 2b5101a56d test: add individual event filtering test (#4465)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to add a new test in `get-events.test.ts` for getting individual events with filters.

### Detailed summary
- Added a new test for getting individual events with filters in `get-events.test.ts`
- Removed a `console.log` statement in `coinbaseMobileSDK.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-09 04:30:57 +00:00
Joaquim VergesandGitHub f1d087e24e feat: cb smart wallet for react native (#4094) 2024-09-09 13:12:03 +12:00
MananTank 9d84aff266 Add deployed contracts page in teams layout (#4440)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to introduce a new component `Progress` using `@radix-ui/react-progress`, update styles, add a new layout component, and improve import handling.

### Detailed summary
- Added a new `Progress` component using `@radix-ui/react-progress`
- Updated styles for various components
- Introduced a new layout component for team/project contracts
- Improved import handling with `useDashboardRouter` usage

> The following files were skipped due to too many changes: `apps/dashboard/src/components/dashboard/StepsCard.stories.tsx`, `apps/dashboard/src/@/components/ui/select.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/contracts/_components/GetStartedWithContractsDeploy.tsx`, `apps/dashboard/src/components/dashboard/StepsCard.tsx`, `apps/dashboard/src/pages/dashboard/contracts/deploy.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-08 16:13:56 +00:00
MykandGitHub e3b4b4fc8c Fix starter template link for modular contracts (#4455)
Signed-off-by: Myk <[email protected]>
2024-09-08 08:26:22 -07:00
joaquim-verges f3a6179e37 fix: use themable SVGs in react native for common icons (#4462)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to fix the common icons theme in React Native components.

### Detailed summary
- Updated icons in `WalletImage.tsx` and `ConnectModal.tsx`
- Refactored `getAuthProviderImage` function
- Added new `PASSKEY_ICON` in `svgs.ts`

> The following files were skipped due to too many changes: `packages/thirdweb/src/react/core/utils/walletIcon.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-08 10:24:19 +00:00
kien-ngo f66bccaba3 [Dashboard] Refactor some code for the Explorer (#4438)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds state management for execution mode (read, write, simulate) and refactors error and data handling in `InteractiveAbiFunction`.

### Detailed summary
- Added `useState` for execution mode
- Refactored error and data handling based on execution mode
- Improved formatting of response data
- Updated button disable conditions for simulation and transaction handling

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-08 05:52:42 +00:00
MananTank 7dd2869e24 Add Usage page in teams layout (chakra) (#4459)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the settings usage page in the dashboard app.

### Detailed summary
- Refactored usage page to use `SettingsUsagePage` component
- Removed redundant imports and code in `usage.tsx`
- Updated `Page` component to render `SettingsUsagePage`
- Updated `getLayout` and `pageId` in `Page` component

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-07 21:55:32 +00:00
MananTank 0978befdf0 Add credits page in team settings (chakra) (#4458)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to refactor the settings page for gas credits, adding a new component and improving the layout.

### Detailed summary
- Added a new component `SettingsGasCreditsPage` for gas credits settings
- Updated the layout of the gas credits settings page
- Removed unused imports and components
- Renamed `SettingsGasCreditsPage` to `Page` and updated references

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-07 21:55:30 +00:00
MananTank 1fbc1df7f3 Add Billing page in team settings (chakra) (#4457)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR refactors the billing settings pages by introducing a new `SettingsBillingPage` component and updating related components for better organization and structure.

### Detailed summary
- Introduces a new `SettingsBillingPage` component for billing settings
- Updates components to use the new `SettingsBillingPage`
- Removes unnecessary code related to account and router handling

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-07 21:55:27 +00:00
MananTank aae90406a4 Add Engine pages to teams (chakra) (#4456)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on restructuring file paths and components related to engine management in the dashboard.

### Detailed summary
- Moved `engine-access-tokens.tsx` to `permissions` folder
- Added `EngineInstancePageProps` type in `types.ts`
- Created new pages for engine creation and import
- Updated various components related to engine management
- Refactored file paths for better organization

> The following files were skipped due to too many changes: `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/layout.tsx`, `apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/general/GeneralSettingsPage.tsx`, `apps/dashboard/src/@/components/blocks/MobileSidebar.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/page.tsx`, `apps/dashboard/src/app/components/Header/TeamHeader/TeamHeaderUI.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/metrics/page.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/admins/page.tsx`, `apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/_components/sidebar/TeamsMobileNav.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/relayers/page.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/explorer/page.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/webhooks/page.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/access-tokens/page.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/configuration/page.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/contract-subscriptions/page.tsx`, `apps/dashboard/src/pages/dashboard/engine/index.tsx`, `apps/dashboard/src/app/team/[team_slug]/(team)/TeamOverviewPage.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(general)/layout.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/engine/(general)/sidebars.tsx`, `apps/dashboard/src/components/engine/engine-instances-table.tsx`, `apps/dashboard/src/@/components/ChakraProviderSetup.tsx`, `apps/dashboard/src/@/components/ui/NavLink.tsx`, `apps/dashboard/src/app/team/[team_slug]/components/account-button.client.tsx`, `apps/dashboard/src/components/engine/EnginePage.tsx`, `apps/dashboard/src/components/engine/EngineSidebar.tsx`, `apps/dashboard/src/components/engine/create/ConfirmEngineTierDialog.tsx`, `apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/members/InviteSection.tsx`, `apps/dashboard/src/app/team/[team_slug]/(team)/~/settings/members/ManageMembersSection.tsx`, `apps/dashboard/src/components/engine/engine-list.tsx`, `apps/dashboard/src/components/engine/import/EngineImportPage.tsx`, `apps/dashboard/src/components/engine/create/CreateEnginePage.tsx`, `apps/dashboard/src/pages/dashboard/engine/import.tsx`, `apps/dashboard/src/pages/dashboard/engine/create.tsx`, `apps/dashboard/src/components/engine/EnginePageLayout.tsx`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-07 21:55:25 +00:00
jnsdls 2aadeb7266 [Update] Add null check for code in CodeBlock component (#4460) 2024-09-07 20:39:16 +00:00
joaquim-verges c546b65e17 export createAndSignUserOp (#4454)
## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
This PR exposes a new utility function `createAndSignUserOp` in `thirdweb` package.

### Detailed summary
- Added `createAndSignUserOp` utility function
- Imported `toSerializableTransaction`
- Updated function `getAccountNonce`
- Added `createAndSignUserOp` function with detailed documentation

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
2024-09-07 09:17:29 +00:00
Joe-ThirdwebandGitHub 88f89f2d84 Joe ancient8 + Donatuz OP Addition (#4448) 2024-09-07 19:28:31 +12:00
696 changed files with 15770 additions and 9619 deletions
+7 -1
View File
@@ -5,7 +5,7 @@ module.exports = {
"plugin:import/typescript",
"plugin:@next/next/recommended",
"plugin:promise/recommended",
"plugin:storybook/recommended"
"plugin:storybook/recommended",
],
rules: {
"react-compiler/react-compiler": "error",
@@ -87,6 +87,12 @@ module.exports = {
message:
'Use `import { useDashboardRouter } from "@/lib/DashboardRouter";` instead',
},
{
name: "lucide-react",
importNames: ["Link", "Table", "Sidebar"],
message:
'This is likely a mistake. If you really want to import this - postfix the imported name with Icon. Example - "LinkIcon"',
},
],
},
],
+21 -21
View File
@@ -51,28 +51,28 @@ function StoryLayout(props: {
}, [theme]);
return (
<div>
<div className="flex justify-end p-4 border-b gap-2">
<Button
onClick={() => setTheme("dark")}
size="sm"
variant={theme === "dark" ? "default" : "ghost"}
>
<MoonIcon className="size-5" />
</Button>
<QueryClientProvider client={queryClient}>
<div className="min-h-screen flex flex-col bg-background text-foreground min-w-0">
<div className="flex justify-end p-4 border-b gap-2">
<Button
onClick={() => setTheme("dark")}
size="sm"
variant={theme === "dark" ? "default" : "ghost"}
>
<MoonIcon className="size-5" />
</Button>
<Button
onClick={() => setTheme("light")}
size="sm"
variant={theme === "light" ? "default" : "ghost"}
>
<SunIcon className="size-5" />
</Button>
<Button
onClick={() => setTheme("light")}
size="sm"
variant={theme === "light" ? "default" : "ghost"}
>
<SunIcon className="size-5" />
</Button>
</div>
<div className="grow flex flex-col min-w-0">{props.children}</div>
</div>
<QueryClientProvider client={queryClient}>
{props.children}
</QueryClientProvider>
</div>
</QueryClientProvider>
);
}
+15 -12
View File
@@ -32,11 +32,14 @@
"@n8tb1t/use-scroll-position": "^2.0.3",
"@radix-ui/react-alert-dialog": "^1.1.1",
"@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-hover-card": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-separator": "^1.1.0",
@@ -46,7 +49,7 @@
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "1.1.2",
"@react-icons/all-files": "^4.1.0",
"@sentry/nextjs": "^8.26.0",
"@sentry/nextjs": "8.30.0",
"@shazow/whatsabi": "^0.14.1",
"@stripe/react-stripe-js": "^2.7.3",
"@stripe/stripe-js": "^3.5.0",
@@ -66,20 +69,20 @@
"date-fns": "^3.6.0",
"ethers": "5.7.2",
"flat": "^6.0.1",
"framer-motion": "^11.2.12",
"framer-motion": "11.5.4",
"fuse.js": "7.0.0",
"ioredis": "^5.4.1",
"ipaddr.js": "^2.2.0",
"lottie-react": "^2.4.0",
"lucide-react": "^0.399.0",
"next": "14.2.8",
"lucide-react": "0.439.0",
"next": "14.2.9",
"next-plausible": "^3.12.0",
"next-seo": "^6.5.0",
"next-themes": "^0.3.0",
"nextjs-toploader": "^1.6.12",
"papaparse": "^5.4.1",
"pluralize": "^8.0.0",
"posthog-js": "1.128.3",
"posthog-js": "1.161.3",
"posthog-js-opensource": "npm:[email protected]",
"prism-react-renderer": "^2.3.1",
"prismjs": "^1.29.0",
@@ -94,7 +97,7 @@
"react-intersection-observer": "^9.10.3",
"react-markdown": "^9.0.1",
"react-otp-input": "^3.1.1",
"react-query-v5": "npm:@tanstack/react-query@^5.40.1",
"react-query-v5": "npm:@tanstack/react-query@5.55.4",
"react-responsive-carousel": "^3.2.23",
"react-table": "^7.8.0",
"recharts": "^2.12.7",
@@ -115,9 +118,9 @@
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "^1.5.0",
"@next/bundle-analyzer": "^14.2.4",
"@next/eslint-plugin-next": "^14.2.4",
"@playwright/test": "1.46.1",
"@next/bundle-analyzer": "14.2.9",
"@next/eslint-plugin-next": "^14.2.9",
"@playwright/test": "1.47.0",
"@storybook/addon-essentials": "^8.2.9",
"@storybook/addon-interactions": "^8.2.9",
"@storybook/addon-links": "^8.2.9",
@@ -142,16 +145,16 @@
"autoprefixer": "^10.4.19",
"checkly": "^4.8.1",
"eslint": "8.57.0",
"eslint-config-biome": "^1.7.3",
"eslint-config-biome": "1.8.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-promise": "^6.2.0",
"eslint-plugin-react-compiler": "0.0.0-experimental-51a85ea-20240601",
"eslint-plugin-react-compiler": "0.0.0-experimental-9ed098e-20240725",
"eslint-plugin-storybook": "^0.8.0",
"next-sitemap": "^4.2.3",
"next-unused": "^0.0.6",
"storybook": "^8.2.9",
"tailwindcss": "^3.4.10",
"typescript": "5.5.4"
"typescript": "5.6.2"
},
"next-unused": {
"alias": {},
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

-5
View File
@@ -270,11 +270,6 @@ function redirects() {
destination: "/in-app-wallets",
permanent: false,
},
{
source: "/dashboard/connect/ecosystem/:slug((?!create$)[^/]+)",
destination: "/dashboard/connect/ecosystem/:slug/permissions",
permanent: false,
},
// temporarily redirect cli login to support page
{
source: "/cli/login",
@@ -0,0 +1,38 @@
"use client";
import { ChakraProvider, useColorMode } from "@chakra-ui/react";
import { useTheme } from "next-themes";
import { useEffect } from "react";
import chakraTheme from "../../theme";
export function ChakraProviderSetup(props: {
children: React.ReactNode;
}) {
return (
<ChakraProvider theme={chakraTheme}>
{props.children}
<SyncTheme />
</ChakraProvider>
);
}
function SyncTheme() {
const { theme, setTheme } = useTheme();
const { setColorMode } = useColorMode();
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
setColorMode(theme === "light" ? "light" : "dark");
}, [setColorMode, theme]);
// handle dashboard with now old "system" set
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
if (theme === "system") {
setTheme("dark");
setColorMode("dark");
}
}, [theme, setTheme, setColorMode]);
return null;
}
@@ -2,18 +2,44 @@
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { ChevronDownIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import { useState } from "react";
import { cn } from "../../lib/utils";
import { NavLink } from "../ui/NavLink";
import type { SidebarLink } from "./Sidebar";
export function MobileSidebar(props: {
links?: { href: string; label: React.ReactNode }[];
links?: SidebarLink[];
footer?: React.ReactNode;
trigger: React.ReactNode;
trigger?: React.ReactNode;
triggerClassName?: string;
}) {
const [isOpen, setIsOpen] = useState(false);
const pathname = usePathname();
const activeLink = props.links?.find((link) => {
if (link.exactMatch) {
return link.href === pathname;
}
return pathname?.startsWith(link.href);
});
const defaultTrigger = (
<Button
className={cn(
"w-full lg:hidden text-left justify-between gap-2 bg-muted/50",
props.triggerClassName,
)}
variant="outline"
>
{activeLink?.label}
<ChevronDownIcon className="size-4 text-muted-foreground" />
</Button>
);
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>{props.trigger}</DialogTrigger>
<DialogTrigger asChild>{props.trigger || defaultTrigger}</DialogTrigger>
<DialogContent
className="p-4 rounded-t-xl rounded-b-none"
dialogCloseClassName="hidden"
@@ -31,6 +57,8 @@ export function MobileSidebar(props: {
href={link.href}
className="!text-left justify-start flex gap-2 h-auto py-3"
activeClassName="bg-accent"
exactMatch={link.exactMatch}
tracking={link.tracking}
>
{link.label}
</NavLink>
@@ -1,32 +1,51 @@
import { ExternalLinkIcon } from "lucide-react";
import type React from "react";
import { cn } from "../../lib/utils";
import { NavLink } from "../ui/NavLink";
export type SidebarLink = {
href: string;
label: React.ReactNode;
exactMatch?: boolean;
tracking?: {
category: string;
action: string;
label: string;
};
};
export type SidebarContentProps = {
header?: React.ReactNode;
links: { href: string; label: React.ReactNode; exactMatch?: boolean }[];
links: SidebarLink[];
className?: string;
};
export function Sidebar(props: SidebarContentProps) {
return (
<aside
className={cn("w-[240px] flex-shrink-0 hidden lg:block", props.className)}
className={cn(
"w-[230px] flex-shrink-0 hidden lg:block self-start sticky top-0",
props.className,
)}
>
<div className="sticky top-0 pt-8">
<div className="pt-7">
{props.header}
<div className="flex flex-col border-l-2 border-border">
{props.links?.map((link) => (
<NavLink
key={link.href}
href={link.href}
className="text-sm py-1.5 pl-4 text-muted-foreground hover:text-foreground border-l-2 ml-[-2px] border-transparent hover:border-foreground transition-colors"
activeClassName="text-foreground border-link-foreground"
exactMatch={link.exactMatch}
>
{link.label}
</NavLink>
))}
<div className="flex flex-col gap-1">
{props.links?.map((link) => {
const isExternal = link.href.startsWith("http");
return (
<NavLink
key={link.href}
href={link.href}
className="py-2 hover:bg-muted rounded-md px-3 text-muted-foreground text-sm flex items-center gap-2"
activeClassName="text-foreground"
exactMatch={link.exactMatch}
>
{link.label}
{isExternal && <ExternalLinkIcon className="size-3" />}
</NavLink>
);
})}
</div>
</div>
</aside>
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../stories/utils";
import { SidebarLayout } from "./SidebarLayout";
const meta = {
title: "layouts/SidebarLayout",
component: Story,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<SidebarLayout
sidebarLinks={[
{
href: "/foo",
label: "consectetur",
},
{
href: "/bar",
label: "adipiscing",
},
{
href: "/bar",
label: "eiusmod",
},
{
label: "incididunt",
href: "https://playground.thirdweb.com/",
},
]}
>
<ContentPlaceholder />
</SidebarLayout>
);
}
function ContentPlaceholder() {
return (
<div className="bg-muted grow flex items-center justify-center">
<p className="text-muted-foreground text-3xl"> CHILDREN </p>
</div>
);
}
@@ -0,0 +1,31 @@
import { cn } from "../../lib/utils";
import { MobileSidebar } from "./MobileSidebar";
import { Sidebar, type SidebarLink } from "./Sidebar";
export function SidebarLayout(props: {
sidebarLinks: SidebarLink[];
children: React.ReactNode;
desktopSidebarClassName?: string;
mobileSidebarClassName?: string;
className?: string;
}) {
const { sidebarLinks, children } = props;
return (
<div
className={cn(
"flex flex-col lg:flex-row gap-6 container px-4 w-full grow max-sm:pt-6",
props.className,
)}
>
<Sidebar links={sidebarLinks} className={props.desktopSidebarClassName} />
<MobileSidebar
links={sidebarLinks}
triggerClassName={props.mobileSidebarClassName}
/>
{/* min-w-0 is enough to ensure the content inside does not stretch this container */}
<main className="grow max-sm:w-full flex flex-col lg:pt-8 pb-10 min-w-0">
{children}
</main>
</div>
);
}
@@ -0,0 +1,192 @@
"use client";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
import { useMemo, useState } from "react";
import { ZERO_ADDRESS } from "thirdweb";
import {
Blobbie,
MediaRenderer,
type SocialProfile,
useSocialProfiles,
} from "thirdweb/react";
import { resolveScheme } from "thirdweb/storage";
import { thirdwebClient } from "../../constants/client";
import { cn } from "../../lib/utils";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
export function WalletAddress(props: {
address: string | undefined;
shortenAddress?: boolean;
className?: string;
}) {
// default back to zero address if no address provided
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
const [shortenedAddress, lessShortenedAddress] = useMemo(() => {
return [
props.shortenAddress !== false
? `${address.slice(0, 6)}...${address.slice(-4)}`
: address,
`${address.slice(0, 14)}...${address.slice(-12)}`,
];
}, [address, props.shortenAddress]);
const profiles = useSocialProfiles({
address: address,
client: thirdwebClient,
});
const [isCopied, setIsCopied] = useState(false);
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(address);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
} catch (err) {
console.error("Failed to copy: ", err);
}
};
return (
<HoverCard>
<HoverCardTrigger asChild>
<Button
onClick={(e) => e.stopPropagation()}
variant="link"
className={cn(
"flex flex-row gap-2 items-center px-0",
props.className,
)}
>
{address && (
<WalletAvatar address={address} profiles={profiles.data || []} />
)}
<span className="font-mono cursor-pointer">
{profiles.data?.[0]?.name || shortenedAddress}
</span>
</Button>
</HoverCardTrigger>
<HoverCardContent
className="w-80 border-border"
onClick={(e) => {
// do not close the hover card when clicking anywhere in the content
e.stopPropagation();
}}
>
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold">Wallet Address</h3>
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
className="flex items-center gap-2"
>
{isCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy"}
</Button>
</div>
<p className="text-sm font-mono bg-muted p-2 rounded text-center">
{lessShortenedAddress}
</p>
<h3 className="text-lg font-semibold">Social Profiles</h3>
{profiles.isLoading ? (
<p className="text-sm text-muted-foreground">Loading profiles...</p>
) : !profiles.data?.length ? (
<p className="text-sm text-muted-foreground">No profiles found</p>
) : (
profiles.data?.map((profile) => (
<div
className="flex flex-row gap-2 items-center"
key={profile.type + profile.name}
>
{profile.avatar &&
(profile.avatar.startsWith("http") ||
profile.avatar?.startsWith("ipfs")) && (
<Avatar>
<AvatarImage
src={resolveScheme({
client: thirdwebClient,
uri: profile.avatar,
})}
alt={profile.name}
/>
{profile.name && (
<AvatarFallback>
{profile.name.slice(0, 2)}
</AvatarFallback>
)}
</Avatar>
)}
<div className="flex flex-col gap-1 w-full">
<div className="flex flex-row gap-4 w-full items-center justify-between">
<h4 className="text-md font-semibold">{profile.name}</h4>
<Badge variant="outline">{profile.type}</Badge>
</div>
{profile.bio && (
<p className="text-sm text-muted-foreground whitespace-normal line-clamp-1">
{profile.bio}
</p>
)}
</div>
</div>
))
)}
<Button
asChild
variant="upsell"
className="text-sm flex flex-row gap-2 items-center"
size="sm"
>
<a
target="_blank"
href="https://blog.thirdweb.com/changelog/introducing-the-social-sdk?ref=dashboard-social-wallet"
rel="noreferrer"
>
Learn more
<ExternalLinkIcon className="size-4" />
</a>
</Button>
</div>
</HoverCardContent>
</HoverCard>
);
}
function WalletAvatar(props: {
address: string;
profiles: SocialProfile[];
}) {
const avatar = useMemo(() => {
return props.profiles.find(
(profile) =>
profile.avatar &&
(profile.avatar.startsWith("http") ||
profile.avatar.startsWith("ipfs")),
)?.avatar;
}, [props.profiles]);
return (
<div className="size-6 overflow-hidden rounded-full">
{avatar ? (
<MediaRenderer
client={thirdwebClient}
src={avatar}
className="size-6"
/>
) : (
<Blobbie address={props.address} size={24} />
)}
</div>
);
}
@@ -3,15 +3,22 @@
import { cn } from "@/lib/utils";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useTrack } from "../../../hooks/analytics/useTrack";
export type NavButtonProps = {
className?: string;
activeClassName?: string;
href: string;
exactMatch?: boolean;
tracking?: {
category: string;
action: string;
label: string;
};
};
export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
const track = useTrack();
const pathname = usePathname();
const isActive = pathname
? props.exactMatch
@@ -23,6 +30,16 @@ export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
href={props.href}
className={cn(props.className, isActive && props.activeClassName)}
target={props.href.startsWith("http") ? "_blank" : undefined}
onClick={() => {
if (props.tracking) {
track({
category: props.tracking.category,
action: props.tracking.action,
label: props.tracking.label,
url: props.href,
});
}
}}
>
{props.children}
</Link>
@@ -1,7 +1,6 @@
.scrollShadowY {
position: absolute;
left: 0;
z-index: 100;
width: 100%;
height: 40px;
pointer-events: none;
@@ -10,7 +9,6 @@
.scrollShadowX {
position: absolute;
top: 0;
z-index: 100;
width: 40px;
height: 100%;
pointer-events: none;
@@ -78,7 +78,7 @@ export function ScrollShadow(props: {
return (
<div
className={cn(props.className, "relative overflow-hidden z-base")}
className={cn(props.className, "relative overflow-hidden")}
ref={wrapperEl}
style={
{
@@ -0,0 +1,50 @@
"use client";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className,
)}
{...props}
/>
));
Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
));
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className,
)}
{...props}
/>
));
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
export { Avatar, AvatarImage, AvatarFallback };
@@ -0,0 +1,115 @@
import { Slot } from "@radix-ui/react-slot";
import { ChevronRight, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
const Breadcrumb = React.forwardRef<
HTMLElement,
React.ComponentPropsWithoutRef<"nav"> & {
separator?: React.ReactNode;
}
>(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
Breadcrumb.displayName = "Breadcrumb";
const BreadcrumbList = React.forwardRef<
HTMLOListElement,
React.ComponentPropsWithoutRef<"ol">
>(({ className, ...props }, ref) => (
<ol
ref={ref}
className={cn(
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
className,
)}
{...props}
/>
));
BreadcrumbList.displayName = "BreadcrumbList";
const BreadcrumbItem = React.forwardRef<
HTMLLIElement,
React.ComponentPropsWithoutRef<"li">
>(({ className, ...props }, ref) => (
<li
ref={ref}
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
));
BreadcrumbItem.displayName = "BreadcrumbItem";
const BreadcrumbLink = React.forwardRef<
HTMLAnchorElement,
React.ComponentPropsWithoutRef<"a"> & {
asChild?: boolean;
}
>(({ asChild, className, ...props }, ref) => {
const Comp = asChild ? Slot : "a";
return (
<Comp
ref={ref}
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
);
});
BreadcrumbLink.displayName = "BreadcrumbLink";
const BreadcrumbPage = React.forwardRef<
HTMLSpanElement,
React.ComponentPropsWithoutRef<"span">
>(({ className, ...props }, ref) => (
<span
ref={ref}
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
));
BreadcrumbPage.displayName = "BreadcrumbPage";
const BreadcrumbSeparator = ({
children,
className,
...props
}: React.ComponentProps<"li">) => (
<li
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
);
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
const BreadcrumbEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More</span>
</span>
);
BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
};
@@ -20,6 +20,8 @@ const buttonVariants = cva(
"bg-secondary hover:bg-secondary/80 text-semibold text-secondary-foreground ",
ghost: "hover:bg-accent text-semibold hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline text-semibold",
upsell:
"bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200",
},
size: {
default: "h-10 px-4 py-2",
@@ -0,0 +1,29 @@
"use client";
import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
import * as React from "react";
import { cn } from "@/lib/utils";
const HoverCard = HoverCardPrimitive.Root;
const HoverCardTrigger = HoverCardPrimitive.Trigger;
const HoverCardContent = React.forwardRef<
React.ElementRef<typeof HoverCardPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<HoverCardPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className,
)}
{...props}
/>
));
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
export { HoverCard, HoverCardTrigger, HoverCardContent };
@@ -0,0 +1,28 @@
"use client";
import * as ProgressPrimitive from "@radix-ui/react-progress";
import * as React from "react";
import { cn } from "@/lib/utils";
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;
export { Progress };
@@ -0,0 +1,82 @@
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/select",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const AllVariants: Story = {
args: {},
};
function randomName() {
return Math.random().toString(36).substring(7);
}
function Component() {
return (
<div className="min-h-screen bg-background p-6 text-foregroun flex flex-col gap-10">
<BadgeContainer label="5 items, no placeholder, no label, value filled">
<SelectDemo listItems={5} selectFirst />
</BadgeContainer>
<BadgeContainer label="50 items, no placeholder, no label, value filled">
<SelectDemo listItems={50} selectFirst />
</BadgeContainer>
<BadgeContainer label="5 items, no placeholder, no label, no value">
<SelectDemo listItems={5} />
</BadgeContainer>
<BadgeContainer label="5 items, placeholder, no label, no value">
<SelectDemo listItems={5} placeholder="some placeholder" />
</BadgeContainer>
</div>
);
}
function SelectDemo(props: {
listItems: number;
placeholder?: string;
label?: string;
selectFirst?: boolean;
}) {
const listItems = Array.from({ length: props.listItems }, () => randomName());
return (
<Select value={props.selectFirst ? listItems[0] : undefined}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={props.placeholder} />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{props.label && <SelectLabel> {props.label}</SelectLabel>}
{listItems.map((item) => {
return (
<SelectItem key={item} value={item}>
{item}
</SelectItem>
);
})}
</SelectGroup>
</SelectContent>
</Select>
);
}
+38 -38
View File
@@ -1,7 +1,7 @@
"use client";
import * as SelectPrimitive from "@radix-ui/react-select";
import { Check, ChevronDown, ChevronUp } from "lucide-react";
import { Check, ChevronDown } from "lucide-react";
import * as React from "react";
import { cn } from "@/lib/utils";
@@ -32,40 +32,40 @@ const SelectTrigger = React.forwardRef<
));
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
));
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
// const SelectScrollUpButton = React.forwardRef<
// React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
// React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
// >(({ className, ...props }, ref) => (
// <SelectPrimitive.ScrollUpButton
// ref={ref}
// className={cn(
// "flex cursor-default items-center justify-center py-1",
// className,
// )}
// {...props}
// >
// <ChevronUp className="h-4 w-4" />
// </SelectPrimitive.ScrollUpButton>
// ));
// SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className,
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
));
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName;
// const SelectScrollDownButton = React.forwardRef<
// React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
// React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
// >(({ className, ...props }, ref) => (
// <SelectPrimitive.ScrollDownButton
// ref={ref}
// className={cn(
// "flex cursor-default items-center justify-center py-1",
// className,
// )}
// {...props}
// >
// <ChevronDown className="h-4 w-4" />
// </SelectPrimitive.ScrollDownButton>
// ));
// SelectScrollDownButton.displayName =
// SelectPrimitive.ScrollDownButton.displayName;
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
@@ -95,7 +95,7 @@ const SelectContent = React.forwardRef<
position={position}
{...props}
>
<SelectScrollUpButton />
{/* <SelectScrollUpButton /> */}
<SelectPrimitive.Viewport
className={cn(
"p-1",
@@ -105,7 +105,7 @@ const SelectContent = React.forwardRef<
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
{/* <SelectScrollDownButton /> */}
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
));
@@ -167,6 +167,6 @@ export {
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
// SelectScrollUpButton,
// SelectScrollDownButton,
};
@@ -36,7 +36,7 @@ export const Mobile: Story = {
function Component() {
return (
<div className="min-h-screen bg-background p-6 text-foregroun flex flex-col gap-6 max-w-[1000px] mx-auto">
<div className="flex flex-col gap-6 lg:max-w-[1000px] lg:mx-auto min-w-0 py-6 px-4">
<BadgeContainer label="Normal">
<TableDemo />
</BadgeContainer>
@@ -3,6 +3,7 @@
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { useCallback, useRef, useState } from "react";
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
import { Button } from "./button";
@@ -177,3 +178,30 @@ function useUnderline<El extends HTMLElement>() {
return { containerRef, lineRef, activeTabRef };
}
export function TabPathLinks(props: {
links: {
name: string;
path: string;
exactMatch?: boolean;
isDisabled?: boolean;
}[];
className?: string;
tabContainerClassName?: string;
shadowColor?: string;
}) {
const pathname = usePathname() || "";
const { links, ...restProps } = props;
return (
<TabLinks
{...restProps}
links={links.map((l) => ({
name: l.name,
href: l.path,
isActive: l.exactMatch
? pathname === l.path
: pathname.startsWith(l.path),
}))}
/>
);
}
@@ -7,15 +7,17 @@ import type { ComponentWithChildren } from "types/component-with-children";
interface AdminOnlyProps {
contract: ThirdwebContract;
failOpen?: boolean;
fallback?: JSX.Element;
}
export const AdminOnly: ComponentWithChildren<AdminOnlyProps> = ({
children,
contract,
failOpen = true,
fallback,
}) => {
const isAdmin = useIsAdmin(contract);
const isAdmin = useIsAdmin(contract, failOpen);
if (!isAdmin) {
return fallback ?? null;
}
@@ -3,7 +3,7 @@ import { hasRole } from "thirdweb/extensions/permissions";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import type { RequiredParam } from "utils/types";
export function useIsAdmin(contract: ThirdwebContract) {
export function useIsAdmin(contract: ThirdwebContract, failOpen = true) {
const address = useActiveAccount()?.address;
const { data: userIsAdmin, isError } = useReadContract(hasRole, {
contract,
@@ -14,7 +14,7 @@ export function useIsAdmin(contract: ThirdwebContract) {
// If the request results in an error, it's likely that the contract is a non-thirdweb contract
// in that case we return `true` anyway
return userIsAdmin || isError;
return userIsAdmin || (failOpen && isError);
}
export function useIsAdminOrSelf(
@@ -1,7 +1,26 @@
import { MobileSidebar } from "@/components/blocks/MobileSidebar";
import { Sidebar } from "@/components/blocks/Sidebar";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { ArrowLeftIcon, MenuIcon, TicketCheckIcon } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
ArrowLeftIcon,
ChevronDownIcon,
MenuIcon,
TicketCheckIcon,
} from "lucide-react";
import type { Metadata } from "next";
import Link from "next/link";
import { redirect } from "next/navigation";
@@ -59,116 +78,142 @@ export default async function ChainPageLayout({
];
return (
<div className="container flex flex-row h-full">
<Sidebar
header={
<div className="flex items-center gap-1.5 mb-5 flex-wrap">
<Link
href="/chainlist"
className="text-muted-foreground hover:text-foreground text-sm"
>
Chainlist
</Link>
<span className="text-muted-foreground text-sm"> / </span>
<Link href={`/${chain.slug}`} className="text-foreground text-sm">
{chain.name.replace("Mainnet", "")}
</Link>
</div>
}
links={sidebarLinks}
/>
<div className="flex flex-col w-full pb-10">
{/* Icon + Background */}
<ChainHeader
headerImageUrl={chainMetadata?.headerImgUrl}
logoUrl={chain.icon?.url}
chain={chain}
/>
<div className="flex lg:hidden justify-end">
<MobileSidebar
links={sidebarLinks}
trigger={
<Button size="icon" variant="outline">
<MenuIcon strokeWidth={1} />
</Button>
}
footer={
<div className="mt-5 border-t pt-6 pb-2">
<>
<div className="border-b border-border h-14 pl-7 flex">
<Breadcrumb className="my-auto">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/chainlist">Chainlist</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<Link
href="/chainlist"
className="px-2 text-sm flex justify-start gap-2 items-center text-muted-foreground hover:text-foreground"
href={
chain.testnet
? "/chainlist/testnets"
: "/chainlist/mainnets"
}
>
<ArrowLeftIcon className="size-4" />
View all chains
{chain.testnet ? "Testnets" : "Mainnets"}
</Link>
</div>
}
<DropdownMenuTrigger className="flex items-center gap-1">
<ChevronDownIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>
<Link href="/chainlist/mainnets">Mainnets</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/chainlist/testnets">Testnets</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>
{chain.name.replace("Mainnet", "")}
</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<div className="container flex flex-row h-full">
<Sidebar links={sidebarLinks} />
<div className="flex flex-col w-full pb-10">
{/* Icon + Background */}
<ChainHeader
headerImageUrl={chainMetadata?.headerImgUrl}
logoUrl={chain.icon?.url}
chain={chain}
/>
</div>
<div className="h-4 md:h-8" />
<div className="flex flex-col gap-3 md:gap-2">
{/* Gas Sponsored badge - Mobile */}
{chainMetadata?.gasSponsored && (
<div className="md:hidden flex">
<GasSponseredBadge />
</div>
)}
{/* Chain name */}
<div className="flex flex-row gap-1.5 items-center">
<h1 className="text-2xl font-semibold lg:text-3xl lg:font-semibold tracking-tight">
{chain.name}
</h1>
{/* Favorite */}
<StarButton
chainId={chain.chainId}
iconClassName="size-5"
className="p-1"
<div className="flex lg:hidden justify-end">
<MobileSidebar
links={sidebarLinks}
trigger={
<Button size="icon" variant="outline">
<MenuIcon strokeWidth={1} />
</Button>
}
footer={
<div className="mt-5 border-t pt-6 pb-2">
<Link
href="/chainlist"
className="px-2 text-sm flex justify-start gap-2 items-center text-muted-foreground hover:text-foreground"
>
<ArrowLeftIcon className="size-4" />
View all chains
</Link>
</div>
}
/>
</div>
{/* Gas Sponsored badge - Desktop */}
<div className="h-4 md:h-8" />
<div className="flex flex-col gap-3 md:gap-2">
{/* Gas Sponsored badge - Mobile */}
{chainMetadata?.gasSponsored && (
<div className="hidden md:block">
<div className="md:hidden flex">
<GasSponseredBadge />
</div>
)}
</div>
{/* description */}
{chainMetadata?.about && (
<p className="text-muted-foreground text-sm lg:text-base mb-2">
{chainMetadata.about}
</p>
)}
{/* Chain name */}
<div className="flex flex-row gap-1.5 items-center">
<h1 className="text-2xl font-semibold lg:text-3xl lg:font-semibold tracking-tight">
{chain.name}
</h1>
{/* mobile action row */}
<div className="sm:hidden w-full">
<div className="grid grid-cols-2 gap-2">
<AddChainToWallet
chain={
// Do not include chain overrides for chain pages
// eslint-disable-next-line no-restricted-syntax
mapV4ChainToV5Chain(chain)
}
{/* Favorite */}
<StarButton
chainId={chain.chainId}
iconClassName="size-5"
className="p-1"
/>
<Button variant="primary">
<Link href="https://thirdweb.com/dashboard" target="_blank">
Get started
</Link>
</Button>
{/* Gas Sponsored badge - Desktop */}
{chainMetadata?.gasSponsored && (
<div className="hidden md:block">
<GasSponseredBadge />
</div>
)}
</div>
{/* description */}
{chainMetadata?.about && (
<p className="text-muted-foreground text-sm lg:text-base mb-2">
{chainMetadata.about}
</p>
)}
{/* mobile action row */}
<div className="sm:hidden w-full">
<div className="grid grid-cols-2 gap-2">
<AddChainToWallet
chain={
// Do not include chain overrides for chain pages
// eslint-disable-next-line no-restricted-syntax
mapV4ChainToV5Chain(chain)
}
/>
<Button variant="primary">
<Link href="https://thirdweb.com/dashboard" target="_blank">
Get started
</Link>
</Button>
</div>
</div>
</div>
</div>
<div className="h-8" />
{children}
<div className="h-8" />
{children}
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,148 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDownIcon } from "lucide-react";
import { headers } from "next/headers";
import Link from "next/link";
import {
AllFilters,
ChainOptionsFilter,
ChainServiceFilter,
} from "../components/client/filters";
import { SearchInput } from "../components/client/search";
import { ChainListView } from "../components/client/view";
import { AddYourChainButton } from "../components/server/add-chain-button";
import {
ChainsData,
type SearchParams,
} from "../components/server/chain-table";
export function generateMetadata(props: {
params: { chain_type: "mainnets" | "testnets" };
}) {
if (props.params.chain_type === "mainnets") {
return {
title: "List of Mainnets | Explorers, Popular Contracts & Chain IDs",
description:
"A list of EVM mainnets with RPCs, smart contracts, block explorers & faucets. Deploy smart contracts to all EVM chains with thirdweb.",
};
}
if (props.params.chain_type === "testnets") {
return {
title: "List of Testnets | Explorers, Popular Contracts & Chain IDs",
description:
"Browse all Testnets across web3 ecosystems, BNB Smart Chain Testnet, Ethereum Sepolia Testnets, Chain IDs, Enabled Services and more. ",
};
}
return {
title: "Chainlist: RPCs, Block Explorers, Faucets",
description:
"A list of EVM networks with RPCs, smart contracts, block explorers & faucets. Deploy smart contracts to all EVM chains with thirdweb.",
};
}
// we use headers() to determine if we should default to table or grid view by checking viewport
// so this page needs to be forced as dynamic
export const dynamic = "force-dynamic";
export default async function ChainListLayout(props: {
params: { chain_type: "mainnets" | "testnets" };
searchParams: SearchParams;
}) {
const headersList = headers();
const viewportWithHint = Number(
headersList.get("Sec-Ch-Viewport-Width") || 0,
);
// default is driven by viewport hint
const activeView = props.searchParams.view
? props.searchParams.view
: viewportWithHint > 1000
? "table"
: "grid";
const searchParams = { type: props.params.chain_type, ...props.searchParams };
return (
<>
{" "}
<div className="border-b border-border h-14 pl-7 flex">
<Breadcrumb className="my-auto">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/chainlist">Chainlist</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<DropdownMenu>
<Link
href={
props.params.chain_type === "testnets"
? "/chainlist/testnets"
: "/chainlist/mainnets"
}
>
{props.params.chain_type === "testnets"
? "Testnets"
: "Mainnets"}
</Link>
<DropdownMenuTrigger className="flex items-center gap-1">
<ChevronDownIcon className="h-4 w-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
<DropdownMenuItem>
<Link href="/chainlist/mainnets">Mainnets</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link href="/chainlist/testnets">Testnets</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
<section className="container mx-auto py-10 px-4 h-full flex flex-col">
<header className="flex flex-col gap-4">
<div className="flex gap-4 flex-col lg:flex-row lg:justify-between lg:items-center">
<div className="flex gap-4 flex-row items-center justify-between lg:justify-start lg:flex-col">
<h1 className="font-semibold text-4xl lg:text-5xl tracking-tighter">
List of{" "}
{props.params.chain_type.charAt(0).toUpperCase() +
props.params.chain_type.slice(1)}
</h1>
<AddYourChainButton className="lg:hidden" />
</div>
<div className="flex flex-row lg:flex-col gap-4 items-end">
<div className="flex flex-row gap-4 w-full">
<SearchInput />
<ChainListView activeView={activeView} />
<AddYourChainButton className="hidden lg:flex" />
</div>
<div className="flex flex-row gap-2">
<AllFilters hideChainType={true} />
<div className="hidden lg:flex flex-row gap-2">
<ChainOptionsFilter />
<ChainServiceFilter />
</div>
</div>
</div>
</div>
</header>
<div className="h-10" />
{/* we used to have suspense + spinner here, that feels more jarring than the page loading _minutely_ slower */}
<ChainsData searchParams={searchParams} activeView={activeView} />
</section>
</>
);
}
@@ -24,7 +24,7 @@ function cleanUrl(url: string) {
return url;
}
export function AllFilters() {
export function AllFilters(props: { hideChainType?: boolean }) {
return (
<Popover>
<PopoverTrigger asChild>
@@ -44,7 +44,7 @@ export function AllFilters() {
<h3 className="font-bold">Filters</h3>
{/* Chain Type */}
<ChainTypeFilter sectionOnly />
{!props.hideChainType && <ChainTypeFilter sectionOnly />}
<Separator />
@@ -126,7 +126,6 @@ export const ChainTypeFilter: React.FC<ChainTypeFilterProps> = ({
sectionOnly,
}) => {
const router = useDashboardRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const value = useMemo(
@@ -134,25 +133,19 @@ export const ChainTypeFilter: React.FC<ChainTypeFilterProps> = ({
[searchParams],
);
const makeUrl = useCallback(
(newValue: string) => {
const params = new URLSearchParams(searchParams || undefined);
switch (newValue) {
case "mainnet":
case "testnet": {
params.set("type", newValue);
break;
}
default: {
params.delete("type");
}
const makeUrl = useCallback((newValue: string) => {
switch (newValue) {
case "mainnets": {
return "/chainlist/mainnets";
}
// reset page alway
params.delete("page");
return cleanUrl(`${pathname}?${params.toString()}`);
},
[pathname, searchParams],
);
case "testnets": {
return "/chainlist/testnets";
}
default: {
return "";
}
}
}, []);
const section = (
<FilterSection title="Chain Type">
@@ -168,12 +161,12 @@ export const ChainTypeFilter: React.FC<ChainTypeFilterProps> = ({
<Label htmlFor="all">All Chains</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="mainnet" id="mainnet" />
<Label htmlFor="mainnet">Mainnets Only</Label>
<RadioGroupItem value="mainnets" id="mainnets" />
<Label htmlFor="mainnest">Mainnets Only</Label>
</div>
<div className="flex items-center gap-2">
<RadioGroupItem value="testnet" id="testnet" />
<Label htmlFor="testnet">Testnets Only</Label>
<RadioGroupItem value="testnets" id="testnets" />
<Label htmlFor="testnets">Testnets Only</Label>
</div>
</RadioGroup>
</FilterSection>
@@ -191,9 +184,9 @@ export const ChainTypeFilter: React.FC<ChainTypeFilterProps> = ({
variant="outline"
className={value === "all" ? undefined : "pr-11 border-primary"}
>
{value === "mainnet"
{value === "mainnets"
? "Mainnets Only"
: value === "testnet"
: value === "testnets"
? "Testnets Only"
: "Chain Type"}
{value === "all" && <ChevronDownIcon className="ml-2 size-4" />}
@@ -0,0 +1,18 @@
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
export function AddYourChainButton(props: { className?: string }) {
return (
<Button asChild variant="default" className={props.className}>
<Link
href="https://share.hsforms.com/1XDi-ieM9Rl6oIkn7ynK6Lgea58c"
target="_blank"
className="flex items-center gap-2"
>
<PlusIcon className="size-4" />
Add your chain
</Link>
</Button>
);
}
@@ -0,0 +1,256 @@
import {
Table,
TableBody,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Fuse from "fuse.js";
import { StarButton } from "../../../components/client/star-button";
import type {
ChainMetadataWithServices,
ChainSupportedService,
} from "../../../types/chain";
import { getChains } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { ChainListCard } from "../server/chainlist-card";
import { ChainListRow } from "../server/chainlist-row";
export type SearchParams = Partial<{
type: "mainnets" | "testnets";
service: ChainSupportedService[] | ChainSupportedService;
includeDeprecated: boolean;
query: string;
page: number;
// maybe later we'll have a page size param?
// pageSize: number;
// table or grid style?
view: "table" | "grid";
}>;
// 24 because it is cleanly divisible by 2,3 and 4 (for card grid)
const DEFAULT_PAGE_SIZE = 24;
const DEFAULT_PAGE = 1;
async function getChainsToRender(params: SearchParams) {
const chains = await getChains();
// sort the chains
const sortedChains = chains.sort((a, b) => {
// sort by number of services first
const aServices = a.services.filter((s) => s.enabled).length;
const bServices = b.services.filter((s) => s.enabled).length;
if (aServices > bServices) {
return -1;
}
if (aServices < bServices) {
return 1;
}
// if they have the same number of services, sort by chainId
if (a.chainId > b.chainId) {
return 1;
}
if (a.chainId < b.chainId) {
return -1;
}
return 0;
});
const filteredChains: ChainMetadataWithServices[] = [];
for (const chain of sortedChains) {
// handle deprecated chains
if (!params.includeDeprecated) {
// if chain is deprecated, return false to filter it out
if (chain.status === "deprecated") {
// skip to the next chain
continue;
}
}
// handle testnet and mainnet filter cases (if )
if (params.type) {
// if the filter is testnet and the chain is not a testnet, return false to filter it out
if (params.type === "testnets" && !chain.testnet) {
// skip to the next chain
continue;
}
// if the filter is mainnet and the chain is a testnet, return false to filter it out
if (params.type === "mainnets" && chain.testnet) {
// skip to the next chain
continue;
}
}
// handle services filter (if no filter set, all chains pass through here)
if (params.service) {
const urlServiceArray = Array.isArray(params.service)
? params.service
: [params.service];
// if the chain does not have all of the services in the filter, return false to filter it out
if (
!urlServiceArray.every((service) => {
return chain.services.find((s) => s.enabled && s.service === service);
})
) {
// skip to the next chain
continue;
}
}
// if we got here, push the chain to the filtered chains array
filteredChains.push(chain);
}
const totalCount = chains.length;
const filteredCount = filteredChains.length;
if (params.query) {
const fuse = new Fuse(filteredChains, {
keys: [
{
name: "name",
weight: 2,
},
{
name: "chainId",
weight: 1,
},
],
threshold: 0.2,
});
return {
chainsToRender: fuse
.search(params.query, {
limit: DEFAULT_PAGE_SIZE,
})
.map((e) => e.item),
totalCount,
filteredCount,
};
}
return {
chainsToRender: filteredChains,
totalCount,
filteredCount,
};
}
export async function ChainsData(props: {
searchParams: SearchParams;
activeView: "table" | "grid";
}) {
const { chainsToRender, totalCount, filteredCount } = await getChainsToRender(
props.searchParams,
);
// pagination
const totalPages = Math.ceil(chainsToRender.length / DEFAULT_PAGE_SIZE);
const activePage = Number(props.searchParams.page || DEFAULT_PAGE);
const pageSize = DEFAULT_PAGE_SIZE;
const startIndex = (activePage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedChains = chainsToRender.slice(startIndex, endIndex);
return (
<>
<main>
{/* empty state */}
{paginatedChains.length === 0 ? (
<div className="border p-8 h-[300px] lg:h-[500px] flex justify-center items-center rounded-lg">
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer>
<Table>
<TableHeader>
<TableRow>
{/* empty space for the icon */}
<TableHead />
<TableHead>Name</TableHead>
<TableHead>Chain ID</TableHead>
<TableHead>Native Token</TableHead>
<TableHead>Enabled Services</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedChains.map((chain) => (
<ChainListRow
key={chain.chainId}
chainId={chain.chainId}
chainName={chain.name}
chainSlug={chain.slug}
currencySymbol={chain.nativeCurrency.symbol}
enabledServices={chain.services
.filter((c) => c.enabled)
.map((c) => c.service)}
isDeprecated={chain.status === "deprecated"}
favoriteButton={
<div className="relative h-6 w-6">
<StarButton
chainId={chain.chainId}
className="absolute z-10 top-0 h-full w-full left-0"
/>
</div>
}
iconUrl={chain.icon?.url}
/>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<ul className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{paginatedChains.map((chain) => (
<li key={chain.chainId} className="h-full">
<ChainListCard
key={chain.chainId}
chainId={chain.chainId}
chainName={chain.name}
chainSlug={chain.slug}
currencySymbol={chain.nativeCurrency.symbol}
enabledServices={chain.services
.filter((c) => c.enabled)
.map((c) => c.service)}
isDeprecated={chain.status === "deprecated"}
favoriteButton={
<div className="relative h-6 w-6">
<StarButton
chainId={chain.chainId}
className="absolute z-10 top-0 h-full w-full left-0"
/>
</div>
}
iconUrl={chain.icon?.url}
/>
</li>
))}
</ul>
)}
</main>
<div className="h-10" />
{totalPages > 1 && (
<ChainlistPagination totalPages={totalPages} activePage={activePage} />
)}
<div className="h-4" />
<p className="text-sm text-center text-muted-foreground text-balance">
Showing{" "}
<span className="text-accent-foreground">{paginatedChains.length}</span>{" "}
out of{" "}
{filteredCount !== totalCount ? (
<>
<span className="text-accent-foreground">{filteredCount}</span>{" "}
chains that match filters. (Total:{" "}
<span className="text-accent-foreground">{totalCount}</span>)
</>
) : (
<>
<span className="text-accent-foreground">{totalCount}</span> chains.
</>
)}
</p>
</>
);
}
@@ -1,154 +1,15 @@
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Fuse from "fuse.js";
import { PlusIcon } from "lucide-react";
import type { Metadata } from "next";
import { headers } from "next/headers";
import Link from "next/link";
import { StarButton } from "../components/client/star-button";
import type {
ChainMetadataWithServices,
ChainSupportedService,
} from "../types/chain";
import { getChains } from "../utils";
import {
AllFilters,
ChainOptionsFilter,
ChainServiceFilter,
ChainTypeFilter,
} from "./components/client/filters";
import { ChainlistPagination } from "./components/client/pagination";
import { SearchInput } from "./components/client/search";
import { ChainListView } from "./components/client/view";
import { ChainListCard } from "./components/server/chainlist-card";
import { ChainListRow } from "./components/server/chainlist-row";
type SearchParams = Partial<{
type: "mainnet" | "testnet";
service: ChainSupportedService[] | ChainSupportedService;
includeDeprecated: boolean;
query: string;
page: number;
// maybe later we'll have a page size param?
// pageSize: number;
// table or grid style?
view: "table" | "grid";
}>;
// 24 because it is cleanly divisible by 2,3 and 4 (for card grid)
const DEFAULT_PAGE_SIZE = 24;
const DEFAULT_PAGE = 1;
async function getChainsToRender(params: SearchParams) {
const chains = await getChains();
// sort the chains
const sortedChains = chains.sort((a, b) => {
// sort by number of services first
const aServices = a.services.filter((s) => s.enabled).length;
const bServices = b.services.filter((s) => s.enabled).length;
if (aServices > bServices) {
return -1;
}
if (aServices < bServices) {
return 1;
}
// if they have the same number of services, sort by chainId
if (a.chainId > b.chainId) {
return 1;
}
if (a.chainId < b.chainId) {
return -1;
}
return 0;
});
const filteredChains: ChainMetadataWithServices[] = [];
for (const chain of sortedChains) {
// handle deprecated chains
if (!params.includeDeprecated) {
// if chain is deprecated, return false to filter it out
if (chain.status === "deprecated") {
// skip to the next chain
continue;
}
}
// handle testnet and mainnet filter cases (if )
if (params.type) {
// if the filter is testnet and the chain is not a testnet, return false to filter it out
if (params.type === "testnet" && !chain.testnet) {
// skip to the next chain
continue;
}
// if the filter is mainnet and the chain is a testnet, return false to filter it out
if (params.type === "mainnet" && chain.testnet) {
// skip to the next chain
continue;
}
}
// handle services filter (if no filter set, all chains pass through here)
if (params.service) {
const urlServiceArray = Array.isArray(params.service)
? params.service
: [params.service];
// if the chain does not have all of the services in the filter, return false to filter it out
if (
!urlServiceArray.every((service) => {
return chain.services.find((s) => s.enabled && s.service === service);
})
) {
// skip to the next chain
continue;
}
}
// if we got here, push the chain to the filtered chains array
filteredChains.push(chain);
}
const totalCount = chains.length;
const filteredCount = filteredChains.length;
if (params.query) {
const fuse = new Fuse(filteredChains, {
keys: [
{
name: "name",
weight: 2,
},
{
name: "chainId",
weight: 1,
},
],
threshold: 0.2,
});
return {
chainsToRender: fuse
.search(params.query, {
limit: DEFAULT_PAGE_SIZE,
})
.map((e) => e.item),
totalCount,
filteredCount,
};
}
return {
chainsToRender: filteredChains,
totalCount,
filteredCount,
};
}
import { AddYourChainButton } from "./components/server/add-chain-button";
import { ChainsData, type SearchParams } from "./components/server/chain-table";
export const metadata: Metadata = {
title: "Chainlist: RPCs, Block Explorers, Faucets",
@@ -207,136 +68,3 @@ export default function ChainListPage(props: { searchParams: SearchParams }) {
</section>
);
}
async function ChainsData(props: {
searchParams: SearchParams;
activeView: "table" | "grid";
}) {
const { chainsToRender, totalCount, filteredCount } = await getChainsToRender(
props.searchParams,
);
// pagination
const totalPages = Math.ceil(chainsToRender.length / DEFAULT_PAGE_SIZE);
const activePage = Number(props.searchParams.page || DEFAULT_PAGE);
const pageSize = DEFAULT_PAGE_SIZE;
const startIndex = (activePage - 1) * pageSize;
const endIndex = startIndex + pageSize;
const paginatedChains = chainsToRender.slice(startIndex, endIndex);
return (
<>
<main>
{/* empty state */}
{paginatedChains.length === 0 ? (
<div className="border p-8 h-[300px] lg:h-[500px] flex justify-center items-center rounded-lg">
<p className="text-2xl">No Results found</p>
</div>
) : props.activeView === "table" ? (
<TableContainer>
<Table>
<TableHeader>
<TableRow>
{/* empty space for the icon */}
<TableHead />
<TableHead>Name</TableHead>
<TableHead>Chain ID</TableHead>
<TableHead>Native Token</TableHead>
<TableHead>Enabled Services</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedChains.map((chain) => (
<ChainListRow
key={chain.chainId}
chainId={chain.chainId}
chainName={chain.name}
chainSlug={chain.slug}
currencySymbol={chain.nativeCurrency.symbol}
enabledServices={chain.services
.filter((c) => c.enabled)
.map((c) => c.service)}
isDeprecated={chain.status === "deprecated"}
favoriteButton={
<div className="relative h-6 w-6">
<StarButton
chainId={chain.chainId}
className="absolute z-10 top-0 h-full w-full left-0"
/>
</div>
}
iconUrl={chain.icon?.url}
/>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<ul className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{paginatedChains.map((chain) => (
<li key={chain.chainId} className="h-full">
<ChainListCard
key={chain.chainId}
chainId={chain.chainId}
chainName={chain.name}
chainSlug={chain.slug}
currencySymbol={chain.nativeCurrency.symbol}
enabledServices={chain.services
.filter((c) => c.enabled)
.map((c) => c.service)}
isDeprecated={chain.status === "deprecated"}
favoriteButton={
<div className="relative h-6 w-6">
<StarButton
chainId={chain.chainId}
className="absolute z-10 top-0 h-full w-full left-0"
/>
</div>
}
iconUrl={chain.icon?.url}
/>
</li>
))}
</ul>
)}
</main>
<div className="h-10" />
{totalPages > 1 && (
<ChainlistPagination totalPages={totalPages} activePage={activePage} />
)}
<div className="h-4" />
<p className="text-sm text-center text-muted-foreground text-balance">
Showing{" "}
<span className="text-accent-foreground">{paginatedChains.length}</span>{" "}
out of{" "}
{filteredCount !== totalCount ? (
<>
<span className="text-accent-foreground">{filteredCount}</span>{" "}
chains that match filters. (Total:{" "}
<span className="text-accent-foreground">{totalCount}</span>)
</>
) : (
<>
<span className="text-accent-foreground">{totalCount}</span> chains.
</>
)}
</p>
</>
);
}
function AddYourChainButton(props: { className?: string }) {
return (
<Button asChild variant="default" className={props.className}>
<Link
href="https://share.hsforms.com/1XDi-ieM9Rl6oIkn7ynK6Lgea58c"
target="_blank"
className="flex items-center gap-2"
>
<PlusIcon className="size-4" />
Add your chain
</Link>
</Button>
);
}
@@ -174,6 +174,8 @@ const chainMetaRecord = {
"Ancient8 is building an ETH gaming Layer 2 built with OP Stack, offering a suite of Web3 gaming infrastructure tools that serve as the distribution and marketing channel for games globally. With Space3 Game Publishing Platform, Ancient8 Gaming Guild, Reneverse Web3 Ads engine, A8ID, and Gosu Network, Ancient8 is dedicated to onboard millions of gamers to Web3 gaming, while providing unparalleled support to game developers looking to reach more players. Ancient8’s products have helped 100+ Web3 games and 200K+ users better navigate Web3.\n\nAncient8 has raised $10M in total financing from leading investors including Pantera, Dragonfly, Hashed, Makers Fund, Mechanism, Coinbase, IOSG, Jump and Animoca.",
// TODO: add CTA
headerImgUrl: ancient8Banner.src,
cta: OP_CTA,
gasSponsored: true,
},
// mantle
5000: {
@@ -185,6 +187,7 @@ const chainMetaRecord = {
buttonText: "Learn More",
buttonLink: "https://x.com/0xMantleDevs",
},
gasSponsored: true,
},
// rootstock
30: {
@@ -275,12 +278,8 @@ const chainMetaRecord = {
headerImgUrl: thirdwebBanner.src,
about:
"Monetize your passion, your way. Discover the freedom to grow with our creator-focused platform.",
cta: {
backgroundImageUrl: thirdwebCTA.src,
title: "",
buttonLink: "https://www.donatuz.org/",
buttonText: "Learn more",
},
cta: OP_CTA,
gasSponsored: true,
},
//Fuse
122: {
@@ -1,31 +0,0 @@
import { NavLink } from "../../../../components/nav-link.client";
import type { SidebarNavSection } from "./types";
export type SidebarSectionProps = {
section: SidebarNavSection;
};
export const SidebarSection: React.FC<SidebarSectionProps> = ({ section }) => {
return (
<div className="flex flex-col gap-2 h-7 relative">
{/* title element */}
<label className="text-sm text-muted-foreground">{section.title}</label>
<nav>
<ul>
{section.items.map((item) => (
<li key={item.id}>
<NavLink
prefetch={false}
href={item.href}
className="pl-3 border-l-2 h-8 text-sm text-muted-foreground flex items-center hover:text-accent-foreground hover:border-foreground"
activeClassName="border-primary hover:border-primary text-accent-foreground font-semibold"
>
{item.title}
</NavLink>
</li>
))}
</ul>
</nav>
</div>
);
};
@@ -1,65 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { MenuIcon } from "lucide-react";
import { usePathname } from "next/navigation";
import type React from "react";
import { useEffect, useMemo, useState } from "react";
import { SidebarSection } from "./section";
import type { SidebarNavSection } from "./types";
export type SideBarNavProps = {
sections: Array<SidebarNavSection>;
};
export const SidebarNav: React.FC<SideBarNavProps> = ({ sections }) => {
const currentPath = usePathname() || "";
const activeLinkInSections = useMemo(() => {
for (const section of sections) {
return section.items.find((item) => item.href.startsWith(currentPath));
}
}, [sections, currentPath]);
const [isMobileOpen, setIsMobileOpen] = useState(false);
// legitimate use case
// eslint-disable-next-line no-restricted-syntax
useEffect(() => {
// close mobile sidebar when path changes
if (currentPath) {
setIsMobileOpen(false);
}
}, [currentPath]);
return (
<aside className="w-full md:w-[250px] lg:w-[300px] border-b md:border-b-0 relative md:sticky top-0 flex-shrink-0">
{/* mobile toggle */}
<div className="flex flex-row items-center justify-between w-full py-4 md:hidden">
<label className="flex items-center h-8 pl-3 text-sm font-semibold border-l-2 text-accent-foreground border-primary">
{activeLinkInSections?.title || "Dashboard"}
</label>
<Button
onClick={() => setIsMobileOpen((prev) => !prev)}
size="icon"
variant="outline"
>
<MenuIcon />
</Button>
</div>
{/* actual link sections */}
<div
className={cn(
"transition-all md:transition-none max-h-0 md:max-h-full md:h-full overflow-hidden md:overflow-auto absolute md:bg-transparent md:relative w-full px-4 md:py-10",
isMobileOpen &&
"h-svh max-h-svh top-full mt-[1px] md:top-0 overflow-auto backdrop-blur-lg md:backdrop-blur-none py-4 bg-background/50 z-50",
)}
>
{sections.map((section) => (
<SidebarSection section={section} key={section.id} />
))}
</div>
</aside>
);
};
@@ -1,12 +0,0 @@
export type SidebarNavLink = {
id: string;
title: string;
href: string;
};
export type SidebarNavSection = {
id: string;
title: string;
// TODO: make sections be able to be in items when we need nested sections
items: Array<SidebarNavLink>;
};
@@ -0,0 +1,38 @@
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
export function ConnectSidebarLayout(props: {
children: React.ReactNode;
}) {
return (
<SidebarLayout
sidebarLinks={[
{
label: "Analytics",
href: "/dashboard/connect/analytics",
},
{
label: "In-App Wallets",
href: "/dashboard/connect/in-app-wallets",
},
{
label: "Ecosystem Wallets",
href: "/dashboard/connect/ecosystem",
},
{
label: "Account Abstraction",
href: "/dashboard/connect/account-abstraction",
},
{
label: "Pay",
href: "/dashboard/connect/pay",
},
{
label: "Playground",
href: "https://playground.thirdweb.com/connect/sign-in/button",
},
]}
>
{props.children}
</SidebarLayout>
);
}
@@ -0,0 +1,99 @@
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { BookMarkedIcon, ExternalLinkIcon, MoveRightIcon } from "lucide-react";
import { cookies } from "next/headers";
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
import headerImage from "./assets/header.png";
import { fetchEcosystemList } from "./utils/fetchEcosystemList";
export async function EcosystemLandingPage(props: {
ecosystemLayoutPath: string;
}) {
const cookiesManager = cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const authToken = activeAccount
? cookiesManager.get(`${COOKIE_PREFIX_TOKEN}${activeAccount}`)?.value
: null;
// if user is logged in and has an ecosystem, redirect to the first ecosystem
if (authToken) {
const ecosystems = await fetchEcosystemList(authToken).catch((err) => {
console.error("failed to fetch ecosystems", err);
return [];
});
if (ecosystems.length > 0) {
redirect(`${props.ecosystemLayoutPath}/${ecosystems[0].slug}`);
}
}
// otherwise we fall through to the page
return (
<main className="container flex flex-col max-w-2xl gap-8 pb-6 mx-auto">
<Image
src={headerImage}
alt="Ecosystems"
sizes="100vw"
className="w-full"
/>
<div className="flex flex-col gap-2 text-left">
<h2 className="text-2xl font-bold sm:text-3xl text-foreground text-balance">
One wallet, a whole ecosystem of apps and games
</h2>
<p className="text-muted-foreground">
With Ecosystem Wallets, your users can access their assets across
hundreds of apps and games within your ecosystem. You can control
which apps join your ecosystem and how their users interact with your
wallet.
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4">
{!authToken ? (
<ToolTipLabel label="Connect your wallet to create an ecosystem">
<Button variant="primary" className="opacity-50">
Create Ecosystem
</Button>
</ToolTipLabel>
) : (
<Button variant="primary" asChild>
<Link href={`${props.ecosystemLayoutPath}/create`}>
Create Ecosystem
</Link>
</Button>
)}
<Button variant="outline" asChild>
<Link
href="https://portal.thirdweb.com/connect/ecosystems/overview"
target="_blank"
className="flex flex-row gap-2"
>
Read The Docs
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
</div>
<div className="w-full h-0 my-2 border-t" />
<Link
href="https://portal.thirdweb.com/connect/ecosystems/overview"
className="mb-6 group"
>
<Card className="flex flex-col gap-3 p-6 md:p-8 md:px-10">
<div className="flex items-center gap-2">
<BookMarkedIcon className="size-5 text-muted-foreground" />
<h4 className="text-lg font-bold">Learn more</h4>
</div>
<p className="text-muted-foreground text-md">
Learn how to create and manage Ecosystem Wallets in our docs.
</p>
<div className="flex flex-row items-center gap-2 mt-4 transition-all group-hover:gap-3 group-hover:text-primary/80 text-primary text-md">
View Docs <MoveRightIcon className="size-4" />
</div>
</Card>
</Link>
</main>
);
}
@@ -0,0 +1,19 @@
"use client";
import { EcosystemPartnersSection } from "./components/server/ecosystem-partners-section";
import { IntegrationPermissionsSection } from "./components/server/integration-permissions-section";
import { useEcosystem } from "./hooks/use-ecosystem";
export function EcosystemPermissionsPage({
params,
}: { params: { slug: string } }) {
const { ecosystem } = useEcosystem({ slug: params.slug });
return (
<div className="flex flex-col gap-12">
<IntegrationPermissionsSection ecosystem={ecosystem} />
{ecosystem?.permission === "PARTNER_WHITELIST" && (
<EcosystemPartnersSection ecosystem={ecosystem} />
)}
</div>
);
}
@@ -0,0 +1,42 @@
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getAddress } from "thirdweb";
import { fetchEcosystem } from "../../utils/fetchEcosystem";
import { EcosystemHeader } from "./components/client/ecosystem-header.client";
export async function EcosystemLayoutSlug({
children,
params,
ecosystemLayoutPath,
}: {
children: React.ReactNode;
params: { slug: string };
ecosystemLayoutPath: string;
}) {
const cookiesManager = cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const authToken = activeAccount
? cookies().get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount))?.value
: null;
if (!authToken) {
redirect(ecosystemLayoutPath);
}
const ecosystem = await fetchEcosystem(params.slug, authToken);
if (!ecosystem) {
redirect(ecosystemLayoutPath);
}
return (
<div className="flex flex-col w-full gap-10">
<EcosystemHeader
ecosystem={ecosystem}
ecosystemLayoutPath={ecosystemLayoutPath}
/>
{children}
</div>
);
}
@@ -60,7 +60,10 @@ function EcosystemAlertBanner({ ecosystem }: { ecosystem: Ecosystem }) {
}
}
function EcosystemSelect(props: { ecosystem: Ecosystem }) {
function EcosystemSelect(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
}) {
const { data: ecosystems, isLoading } = useEcosystemList();
return isLoading ? (
@@ -81,7 +84,7 @@ function EcosystemSelect(props: { ecosystem: Ecosystem }) {
{ecosystems?.map((ecosystem) => (
<DropdownMenuItem key={ecosystem.id} asChild>
<Link
href={`/dashboard/connect/ecosystem/${ecosystem.slug}`}
href={`${props.ecosystemLayoutPath}/${ecosystem.slug}`}
className="relative flex items-center pl-8 pr-3 cursor-pointer"
>
{ecosystem.slug === props.ecosystem.slug && (
@@ -93,7 +96,7 @@ function EcosystemSelect(props: { ecosystem: Ecosystem }) {
))}
</DropdownMenuGroup>
<DropdownMenuSeparator />
<Link href="/dashboard/connect/ecosystem/create" className="">
<Link href={`${props.ecosystemLayoutPath}/create`} className="">
<DropdownMenuItem className="relative flex items-center pl-8 pr-3 cursor-pointer">
<PlusCircleIcon className="absolute w-4 h-4 left-2" />
<div className="truncate">New Ecosystem</div>
@@ -104,7 +107,10 @@ function EcosystemSelect(props: { ecosystem: Ecosystem }) {
);
}
export function EcosystemHeader(props: { ecosystem: Ecosystem }) {
export function EcosystemHeader(props: {
ecosystem: Ecosystem;
ecosystemLayoutPath: string;
}) {
const pathname = usePathname();
const { data: fetchedEcosystem } = useEcosystem({
slug: props.ecosystem.slug,
@@ -166,15 +172,19 @@ export function EcosystemHeader(props: { ecosystem: Ecosystem }) {
</div>
</div>
<div className="flex flex-col justify-between gap-4 md:items-end">
<EcosystemSelect ecosystem={ecosystem} />
<EcosystemSelect
ecosystem={ecosystem}
ecosystemLayoutPath={props.ecosystemLayoutPath}
/>
</div>
</div>
<TabLinks
links={[
{
name: "Permissions",
href: `/dashboard/connect/ecosystem/${ecosystem.slug}/permissions`,
isActive: pathname?.endsWith("/permissions") || false, // pathname will almost never be null: https://nextjs.org/docs/app/api-reference/functions/use-pathname
href: `${props.ecosystemLayoutPath}/${ecosystem.slug}`,
isActive:
pathname === `${props.ecosystemLayoutPath}/${ecosystem.slug}`,
},
{
name: "Analytics (Coming Soon)",
@@ -3,6 +3,7 @@ import { CopyButton } from "@/components/ui/CopyButton";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import {
Table,
TableBody,
TableCell,
TableContainer,
@@ -12,7 +13,7 @@ import {
} from "@/components/ui/table";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Pencil, Table, Trash2 } from "lucide-react";
import { Pencil, Trash2 } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
import type { Ecosystem, Partner } from "../../../../types";
@@ -1,54 +1,16 @@
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { getAddress } from "thirdweb";
import type { Ecosystem } from "../../types";
import { EcosystemHeader } from "./components/client/ecosystem-header.client";
async function fetchEcosystem(slug: string, authToken: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
if (!res.ok) {
const data = await res.json();
console.error(data);
return null;
}
const data = (await res.json()) as { result: Ecosystem };
return data.result;
}
import { EcosystemLayoutSlug } from "./EcosystemSlugLayout";
export default async function Layout({
children,
params,
}: { children: React.ReactNode; params: { slug: string } }) {
const cookiesManager = cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const authToken = activeAccount
? cookies().get(COOKIE_PREFIX_TOKEN + getAddress(activeAccount))?.value
: null;
if (!authToken) {
redirect("/dashboard/connect/ecosystem");
}
const ecosystem = await fetchEcosystem(params.slug, authToken);
if (!ecosystem) {
redirect("/dashboard/connect/ecosystem");
}
return (
<div className="flex flex-col w-full gap-10 px-2 py-10 sm:px-4">
<EcosystemHeader ecosystem={ecosystem} />
<EcosystemLayoutSlug
params={params}
ecosystemLayoutPath="/dashboard/connect/ecosystem"
>
{children}
</div>
</EcosystemLayoutSlug>
);
}
@@ -0,0 +1,5 @@
import { EcosystemPermissionsPage } from "./EcosystemPermissionsPage";
export default function Page({ params }: { params: { slug: string } }) {
return <EcosystemPermissionsPage params={params} />;
}
@@ -1,17 +0,0 @@
"use client";
import { EcosystemPartnersSection } from "../components/server/ecosystem-partners-section";
import { IntegrationPermissionsSection } from "../components/server/integration-permissions-section";
import { useEcosystem } from "../hooks/use-ecosystem";
export default function Page({ params }: { params: { slug: string } }) {
const { ecosystem } = useEcosystem({ slug: params.slug });
return (
<main className="flex flex-col gap-12">
<IntegrationPermissionsSection ecosystem={ecosystem} />
{ecosystem?.permission === "PARTNER_WHITELIST" && (
<EcosystemPartnersSection ecosystem={ecosystem} />
)}
</main>
);
}
@@ -0,0 +1,29 @@
import { CreateEcosystemForm } from "./components/client/create-ecosystem-form.client";
import { EcosystemWalletPricingCard } from "./components/pricing-card";
export function EcosystemCreatePage(props: {
ecosystemLayoutPath: string;
}) {
return (
<div className="flex flex-col w-full gap-6">
<header className="flex flex-col gap-1">
<h2 className="text-3xl font-semibold text-foreground tracking-tight">
Create an Ecosystem
</h2>
<p className="text-muted-foreground">
Create wallets that work across every chain and every app.
</p>
</header>
<main className="grid w-full max-w-sm gap-6 md:max-w-lg lg:max-w-4xl lg:grid-cols-2">
<section className="flex items-start">
<EcosystemWalletPricingCard />
</section>
<section className="mb-12 lg:px-4">
<CreateEcosystemForm
ecosystemLayoutPath={props.ecosystemLayoutPath}
/>
</section>
</main>
</div>
);
}
@@ -42,7 +42,9 @@ const formSchema = z.object({
permission: z.union([z.literal("PARTNER_WHITELIST"), z.literal("ANYONE")]),
});
export function CreateEcosystemForm() {
export function CreateEcosystemForm(props: {
ecosystemLayoutPath: string;
}) {
// When set, the confirmation modal is open the this contains the form data to be submitted
const [formDataToBeConfirmed, setFormDataToBeConfirmed] = useState<
z.infer<typeof formSchema> | undefined
@@ -65,7 +67,7 @@ export function CreateEcosystemForm() {
},
onSuccess: (slug: string) => {
form.reset();
router.push(`/dashboard/connect/ecosystem/${slug}`);
router.push(`${props.ecosystemLayoutPath}/${slug}`);
},
});
@@ -1,25 +1,7 @@
import { CreateEcosystemForm } from "./components/client/create-ecosystem-form.client";
import { EcosystemWalletPricingCard } from "./components/pricing-card";
import { EcosystemCreatePage } from "./EcosystemCreatePage";
export default function Page() {
return (
<div className="flex flex-col w-full gap-10 px-2 py-10 sm:px-4">
<header className="flex flex-col gap-2">
<h2 className="text-4xl font-bold text-foreground">
Create an Ecosystem
</h2>
<p className="text-muted-foreground">
Create wallets that work across every chain and every app.
</p>
</header>
<main className="grid w-full max-w-sm gap-8 md:max-w-lg lg:max-w-4xl xl:gap-12 lg:grid-cols-2">
<section className="flex items-start">
<EcosystemWalletPricingCard />
</section>
<section className="mb-12 lg:px-4">
<CreateEcosystemForm />
</section>
</main>
</div>
<EcosystemCreatePage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
);
}
@@ -1,116 +1,8 @@
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { BookMarkedIcon, ExternalLinkIcon, MoveRightIcon } from "lucide-react";
import { cookies } from "next/headers";
import Image from "next/image";
import Link from "next/link";
import { redirect } from "next/navigation";
import headerImage from "./assets/header.png";
import type { Ecosystem } from "./types";
async function fetchEcosystemList(authToken: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/list`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
if (!res.ok) {
const data = await res.json();
console.error(data);
throw new Error(data?.error?.message ?? "Failed to fetch ecosystems");
}
const data = (await res.json()) as { result: Ecosystem[] };
return data.result;
}
import { EcosystemLandingPage } from "./EcosystemLandingPage";
export default async function Page() {
const cookiesManager = cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const authToken = activeAccount
? cookiesManager.get(`${COOKIE_PREFIX_TOKEN}${activeAccount}`)?.value
: null;
// if user is logged in and has an ecosystem, redirect to the first ecosystem
if (authToken) {
const ecosystems = await fetchEcosystemList(authToken).catch((err) => {
console.error("failed to fetch ecosystems", err);
return [];
});
if (ecosystems.length > 0) {
redirect(`/dashboard/connect/ecosystem/${ecosystems[0].slug}`);
}
}
// otherwise we fall through to the page
return (
<main className="container flex flex-col max-w-2xl gap-8 pb-6 mx-auto">
<Image
src={headerImage}
alt="Ecosystems"
sizes="100vw"
className="w-full"
/>
<div className="flex flex-col gap-2 text-center sm:text-left">
<h2 className="text-2xl font-bold sm:text-3xl text-foreground text-balance">
One wallet, a whole ecosystem of apps and games
</h2>
<p className="text-muted-foreground">
With Ecosystem Wallets, your users can access their assets across
hundreds of apps and games within your ecosystem. You can control
which apps join your ecosystem and how their users interact with your
wallet.
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:gap-4">
{!authToken ? (
<ToolTipLabel label="Connect your wallet to create an ecosystem">
<Button variant="primary" className="opacity-50">
Create Ecosystem
</Button>
</ToolTipLabel>
) : (
<Link href="/dashboard/connect/ecosystem/create">
<Button variant="primary">Create Ecosystem</Button>
</Link>
)}
<Button variant="outline" asChild>
<Link
href="https://portal.thirdweb.com/connect/ecosystems/overview"
target="_blank"
className="flex flex-row gap-2"
>
Read The Docs
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
</div>
<div className="w-full h-0 my-2 border-t" />
<Link
href="https://portal.thirdweb.com/connect/ecosystems/overview"
className="mb-6 group"
>
<Card className="flex flex-col gap-3 p-6 md:p-8 md:px-10">
<div className="flex items-center gap-2">
<BookMarkedIcon className="size-5 text-muted-foreground" />
<h4 className="text-lg font-bold">Learn more</h4>
</div>
<p className="text-muted-foreground text-md">
Learn how to create and manage Ecosystem Wallets in our docs.
</p>
<div className="flex flex-row items-center gap-2 mt-4 transition-all group-hover:gap-3 group-hover:text-primary/80 text-primary text-md">
View Docs <MoveRightIcon className="size-4" />
</div>
</Card>
</Link>
</main>
<EcosystemLandingPage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
);
}
@@ -0,0 +1,20 @@
import type { Ecosystem } from "../types";
export async function fetchEcosystem(slug: string, authToken: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/${slug}`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
if (!res.ok) {
const data = await res.json();
console.error(data);
return null;
}
const data = (await res.json()) as { result: Ecosystem };
return data.result;
}
@@ -0,0 +1,21 @@
import type { Ecosystem } from "../types";
export async function fetchEcosystemList(authToken: string) {
const res = await fetch(
`${process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com"}/v1/ecosystem-wallet/list`,
{
headers: {
Authorization: `Bearer ${authToken}`,
},
},
);
if (!res.ok) {
const data = await res.json();
console.error(data);
throw new Error(data?.error?.message ?? "Failed to fetch ecosystems");
}
const data = (await res.json()) as { result: Ecosystem[] };
return data.result;
}
@@ -1,52 +1,7 @@
import { SidebarNav } from "../components/sidebar/sidebar.client";
import type { SidebarNavSection } from "../components/sidebar/types";
import { ConnectSidebarLayout } from "./DashboardConnectLayout";
const SIDEBAR_SECTIONS = [
{
id: "connect",
title: "Connect",
items: [
{
id: "analytics",
title: "Analytics",
href: "/dashboard/connect/analytics",
},
{
id: "embedded-wallets",
title: "In-App Wallets",
href: "/dashboard/connect/in-app-wallets",
},
{
id: "ecosystem",
title: "Ecosystem Wallets",
href: "/dashboard/connect/ecosystem",
},
{
id: "account-abstraction",
title: "Account Abstraction",
href: "/dashboard/connect/account-abstraction",
},
{
id: "pay-settings",
title: "Pay",
href: "/dashboard/connect/pay",
},
{
id: "playground",
title: "Playground",
href: "https://playground.thirdweb.com/connect/sign-in/button",
},
],
},
] satisfies Array<SidebarNavSection>;
export default function DashboardConnectLayout(props: {
export default function Layout(props: {
children: React.ReactNode;
}) {
return (
<div className="flex flex-col md:flex-row md:h-full container px-4">
<SidebarNav sections={SIDEBAR_SECTIONS} />
{props.children}
</div>
);
return <ConnectSidebarLayout>{props.children}</ConnectSidebarLayout>;
}
@@ -44,10 +44,12 @@ export default async function DashboardConnectPayPage() {
});
return (
<div className="flex flex-col gap-8 py-6 w-full">
<div className="flex flex-col gap-8 w-full">
<div className="flex flex-col lg:flex-row gap-6 justify-between items-start">
<div className="max-w-[800px]">
<h1 className="text-5xl tracking-tight font-bold mb-5">Pay</h1>
<h1 className="text-3xl lg:text-4xl tracking-tight font-bold mb-3">
Pay
</h1>
<p className="text-muted-foreground leading-7">
Pay allows your users to purchase cryptocurrencies and execute
transactions with their credit card or debit card, or with any token
@@ -1,15 +1,11 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { cn } from "@/lib/utils";
import { CircleAlertIcon } from "lucide-react";
import {
Alert,
AlertDescription,
AlertTitle,
} from "../../../@/components/ui/alert";
import { Badge } from "../../../@/components/ui/badge";
import { Button } from "../../../@/components/ui/button";
import { Checkbox } from "../../../@/components/ui/checkbox";
import { Input } from "../../../@/components/ui/input";
import { Label } from "../../../@/components/ui/label";
export const metadata = {
robots: "noindex, nofollow",
@@ -1,6 +1,6 @@
import { type SortBy, type TimeRange, fetchTopContracts } from "lib/search";
import type { Metadata } from "next";
import { Sidebar } from "../../../@/components/blocks/Sidebar";
import { ContractsSidebarLayout } from "../../../core-ui/sidebar/contracts";
import { TrendingContractSection } from "./components/trending-table";
export const metadata: Metadata = {
@@ -25,42 +25,16 @@ export default async function DashboardContractTrendingPage(props: {
});
return (
<div className="container px-4 flex gap-2">
<Sidebar
links={[
{
href: "/dashboard/contracts/deploy",
label: "Deploy",
},
{
href: "/dashboard/contracts/publish",
label: "Publish",
},
{
href: "/explore",
label: "Explore",
},
{
href: "/trending",
label: "Trending",
},
{
href: "/dashboard/contracts/build",
label: "Build",
},
]}
/>
<div className="py-8">
<h1 className="text-3xl md:text-4xl tracking-tight font-semibold mb-5">
Trending Contracts
</h1>
<ContractsSidebarLayout>
<h1 className="text-2xl md:text-3xl tracking-tight font-semibold mb-5">
Trending Contracts
</h1>
<TrendingContractSection
topContracts={topContracts}
searchParams={props.searchParams}
showPagination={true}
/>
</div>
</div>
<TrendingContractSection
topContracts={topContracts}
searchParams={props.searchParams}
showPagination={true}
/>
</ContractsSidebarLayout>
);
}
@@ -0,0 +1,46 @@
"use client";
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
import { useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { useCallback } from "react";
import {
type AccountHeaderCompProps,
AccountHeaderDesktopUI,
AccountHeaderMobileUI,
} from "./AccountHeaderUI";
export function AccountHeader(props: {
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
}) {
const myAccountQuery = useAccount();
const router = useDashboardRouter();
const logout = useCallback(async () => {
// log out the user
try {
await fetch("/api/auth/logout", {
method: "POST",
});
router.push("/login");
} catch (e) {
console.error("Failed to log out", e);
}
}, [router]);
const headerProps: AccountHeaderCompProps = {
teamsAndProjects: props.teamsAndProjects,
email: myAccountQuery.data?.email,
logout: logout,
connectButton: <CustomConnectWallet />,
};
return (
<div>
<AccountHeaderDesktopUI {...headerProps} className="max-lg:hidden" />
<AccountHeaderMobileUI {...headerProps} className="lg:hidden" />
</div>
);
}
@@ -0,0 +1,65 @@
import { Button } from "@/components/ui/button";
import type { Meta, StoryObj } from "@storybook/react";
import { ThirdwebProvider } from "thirdweb/react";
import { teamsAndProjectsStub } from "../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import {
AccountHeaderDesktopUI,
AccountHeaderMobileUI,
} from "./AccountHeaderUI";
const meta = {
title: "Account/Account Header",
component: Variants,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Variants>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {
type: "desktop",
},
};
export const Mobile: Story = {
args: {
type: "mobile",
},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Variants(props: {
type: "mobile" | "desktop";
}) {
const Comp =
props.type === "mobile" ? AccountHeaderMobileUI : AccountHeaderDesktopUI;
return (
<ThirdwebProvider>
<div
className={"flex flex-col gap-6 bg-gray-700 px-4 py-10 min-h-screen"}
>
<BadgeContainer label="Logged in">
<Comp
teamsAndProjects={teamsAndProjectsStub}
email="[email protected]"
logout={() => {}}
connectButton={<ConnectButtonStub />}
/>
</BadgeContainer>
</div>
</ThirdwebProvider>
);
}
function ConnectButtonStub() {
return <Button>Connect</Button>;
}
@@ -0,0 +1,101 @@
import type { Project } from "@/api/projects";
import type { Team } from "@/api/team";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { SecondaryNav } from "../../components/Header/SecondaryNav/SecondaryNav";
import { MobileBurgerMenuButton } from "../../components/MobileBurgerMenuButton";
import { ThirdwebMiniLogo } from "../../components/ThirdwebMiniLogo";
import { TeamAndProjectSelectorPopoverButton } from "../../team/components/TeamHeader/TeamAndProjectSelectorPopoverButton";
import { TeamSelectorMobileMenuButton } from "../../team/components/TeamHeader/TeamSelectorMobileMenuButton";
export type AccountHeaderCompProps = {
className?: string;
email: string | undefined;
logout: () => void;
connectButton: React.ReactNode;
teamsAndProjects: Array<{ team: Team; projects: Project[] }>;
};
export function AccountHeaderDesktopUI(props: AccountHeaderCompProps) {
return (
<header
className={cn(
"flex flex-row gap-2 items-center text-foreground justify-between px-6 py-4",
props.className,
)}
>
<div className="flex items-center gap-2">
<Link href="/team">
<ThirdwebMiniLogo className="h-5" />
</Link>
<SlashSeparator />
<div className="flex items-center gap-1">
<Link
href="/account"
className="font-normal text-sm flex flex-row gap-2 items-center"
>
{/* TODO - replace with account image */}
<div className="bg-muted border border-border size-6 rounded-full" />
<span> My Account </span>
</Link>
<TeamAndProjectSelectorPopoverButton
currentProject={undefined}
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
focus="team-selection"
/>
</div>
</div>
<SecondaryNav
email={props.email}
logout={props.logout}
connectButton={props.connectButton}
/>
</header>
);
}
export function AccountHeaderMobileUI(props: AccountHeaderCompProps) {
return (
<header
className={cn(
"flex flex-row gap-2 items-center text-foreground justify-between px-4 py-4",
props.className,
)}
>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<Link
href={"/account"}
className={cn(
"font-normal text-sm flex flex-row items-center text-foreground gap-2",
)}
>
{/* TODO - replace with account image */}
<div className="bg-muted border border-border size-7 rounded-full" />
<span> My Account </span>
</Link>
<TeamSelectorMobileMenuButton
currentTeam={undefined}
teamsAndProjects={props.teamsAndProjects}
/>
</div>
</div>
<MobileBurgerMenuButton
email={props.email}
logout={props.logout}
connectButton={props.connectButton}
/>
</header>
);
}
function SlashSeparator() {
return <div className="h-5 w-[1.5px] bg-foreground/30 rotate-[25deg] mx-2" />;
}
@@ -0,0 +1,20 @@
import { Input } from "@/components/ui/input";
import { SearchIcon } from "lucide-react";
export function SearchInput(props: {
placeholder: string;
value: string;
onValueChange: (value: string) => void;
}) {
return (
<div className="relative">
<Input
placeholder={props.placeholder}
value={props.value}
onChange={(e) => props.onValueChange(e.target.value)}
className="pl-9 bg-muted/50"
/>
<SearchIcon className="size-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
</div>
);
}
+65
View File
@@ -0,0 +1,65 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
import { AppFooter } from "@/components/blocks/app-footer";
import type React from "react";
import { TWAutoConnect } from "../components/autoconnect";
import { AccountHeader } from "./components/AccountHeader";
export default async function AccountLayout(props: {
children: React.ReactNode;
}) {
return (
<div className="min-h-full flex flex-col bg-background">
<div className="grow flex flex-col">
<HeaderAndNav />
<div className="py-10 border-b border-border">
<div className="max-w-[1100px] mx-auto px-6">
<h1 className="text-3xl tracking-tight font-semibold">
My Account
</h1>
</div>
</div>
<SidebarLayout
className="max-w-[1100px]"
sidebarLinks={[
{
href: "/account",
label: "Overview",
exactMatch: true,
},
{
href: "/account/wallets",
label: "Wallets",
},
{
href: "/account/settings",
label: "Settings",
},
]}
>
{props.children}
</SidebarLayout>
</div>
<TWAutoConnect />
<AppFooter />
</div>
);
}
async function HeaderAndNav() {
const teams = await getTeams();
const teamsAndProjects = await Promise.all(
teams.map(async (team) => ({
team,
projects: await getProjects(team.slug),
})),
);
return (
<div className="bg-muted/50 border-b">
<AccountHeader teamsAndProjects={teamsAndProjects} />
</div>
);
}
@@ -0,0 +1,72 @@
import type { Meta, StoryObj } from "@storybook/react";
import { teamStub } from "../../../stories/stubs";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { AccountTeamsUI } from "./AccountTeamsUI";
const meta = {
title: "Account/Pages/Teams",
component: Variants,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Variants>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {
type: "desktop",
},
};
export const Mobile: Story = {
args: {
type: "mobile",
},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Variants() {
return (
<div className="container py-10 max-w-[1100px] mx-auto w-full flex flex-col gap-10">
<BadgeContainer label="4 Teams">
<AccountTeamsUI
teamsWithRole={[
{
team: teamStub("1", "free"),
role: "MEMBER",
},
{
team: teamStub("2", "pro"),
role: "OWNER",
},
{
team: teamStub("3", "growth"),
role: "MEMBER",
},
{
team: teamStub("4", "growth"),
role: "MEMBER",
},
]}
/>
</BadgeContainer>
<BadgeContainer label="1 Team">
<AccountTeamsUI
teamsWithRole={[
{
team: teamStub("1", "free"),
role: "MEMBER",
},
]}
/>
</BadgeContainer>
</div>
);
}
@@ -0,0 +1,147 @@
"use client";
import type { Team } from "@/api/team";
import type { TeamAccountRole } from "@/api/team-members";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { EllipsisIcon, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { TeamPlanBadge } from "../../components/TeamPlanBadge";
import { getValidTeamPlan } from "../../team/components/TeamHeader/getValidTeamPlan";
import { SearchInput } from "../components/SearchInput";
export function AccountTeamsUI(props: {
teamsWithRole: {
team: Team;
role: TeamAccountRole;
}[];
}) {
const [teamSearchValue, setTeamSearchValue] = useState("");
const teamsToShow = !teamSearchValue
? props.teamsWithRole
: props.teamsWithRole.filter((v) => {
return v.team.name
.toLowerCase()
.includes(teamSearchValue.toLowerCase());
});
return (
<div>
<div className="flex items-start gap-4 flex-col lg:flex-row lg:justify-between">
<div>
<h2 className="font-semibold text-xl tracking-tight mb-0.5">Teams</h2>
<p className="text-muted-foreground text-sm">
The teams that are associated with your thirdweb account
</p>
</div>
<ToolTipLabel label="Coming Soon">
<Button variant="primary" disabled className="max-sm:w-full gap-2">
<PlusIcon className="size-4" />
Create a Team
</Button>
</ToolTipLabel>
</div>
<div className="h-4" />
<SearchInput
placeholder="Search Teams"
value={teamSearchValue}
onValueChange={setTeamSearchValue}
/>
<div className="h-4" />
<ul className="bg-muted/50 border rounded-lg">
{/* Teams List */}
{teamsToShow.map((v) => {
return (
<li
key={v.team.id}
className="border-b border-border p-4 last:border-b-0"
>
<TeamRow team={v.team} role={v.role} />
</li>
);
})}
{/* No Result Found */}
{teamsToShow.length === 0 && (
<div className="p-4 h-[200px] justify-center flex items-center">
<div className="text-center">
<p className="mb-3 text-sm">No teams found</p>
{teamSearchValue && (
<p className="text-sm text-muted-foreground">
Your search for {`"${teamSearchValue}"`} did not match any
teams
</p>
)}
</div>
</div>
)}
</ul>
</div>
);
}
function TeamRow(props: {
team: Team;
role: TeamAccountRole;
}) {
const plan = getValidTeamPlan(props.team);
return (
<div className="flex gap-2 items-center justify-between">
{/* start */}
<div className="flex items-center gap-4">
{/* TODO - render team avatar */}
<div className="border rounded-full size-8 bg-muted" />
<div>
<div className="flex items-center gap-3">
<p className="font-semibold text-sm">{props.team.name}</p>
<TeamPlanBadge plan={plan} />
</div>
<p className="text-muted-foreground text-sm capitalize">
{props.role.toLowerCase()}
</p>
</div>
</div>
{/* end */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="ghost" className="!h-auto !w-auto p-1.5">
<EllipsisIcon className="size-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[180px]">
<DropdownMenuItem>
<Link href={`/team/${props.team.slug}`} className="w-full p-1 ">
View
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link
href={`/team/${props.team.slug}/~/settings`}
className="w-full p-1 "
>
Manange
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
}
+18
View File
@@ -0,0 +1,18 @@
import { getTeams } from "@/api/team";
import { AccountTeamsUI } from "./overview/AccountTeamsUI";
export default async function Page() {
const teams = await getTeams();
const teamsWithRole = teams.map((team) => ({
team,
// TODO fetch the role of current user in this team
role: "MEMBER" as const, // THIS IS A PLACEHOLDER !!!
}));
return (
<div className="grow">
<AccountTeamsUI teamsWithRole={teamsWithRole} />
</div>
);
}
@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import { ThirdwebProvider } from "thirdweb/react";
import { mobileViewport } from "../../../stories/utils";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
const meta = {
title: "Account/Pages/Settings",
component: Variants,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Variants>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {
type: "desktop",
},
};
export const Mobile: Story = {
args: {
type: "mobile",
},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Variants() {
return (
<ThirdwebProvider>
<div className="container py-10 max-w-[1132px] mx-auto w-full flex flex-col gap-10">
<AccountSettingsPageUI
account={{
name: "John Doe",
email: "[email protected]",
}}
/>
</div>
<Toaster richColors />
</ThirdwebProvider>
);
}
@@ -0,0 +1,335 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { EllipsisIcon } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { FileInput } from "../../../components/shared/FileInput";
import { DangerSettingCard } from "../../team/[team_slug]/(team)/~/settings/general/DangerSettingCard";
import { SettingsCard } from "../../team/[team_slug]/(team)/~/settings/general/SettingsCard";
type MinimalAccount = Pick<
Account,
"name" | "email" | "emailConfirmedAt" | "unconfirmedEmail"
>;
export function AccountSettingsPageUI(props: {
account: MinimalAccount;
}) {
return (
<div className="flex flex-col gap-8">
<AccountAvatarFormControl />
<AccountNameFormControl name={props.account.name || ""} />
<AccountEmailFormControl
email={props.account.email || ""}
// TODO - is this correct way to check this?
status={props.account.emailConfirmedAt ? "verified" : "unverified"}
/>
<DeleteAccountCard />
</div>
);
}
function AccountAvatarFormControl() {
const [avatar, setAvatar] = useState<File>(); // TODO: prefill with account avatar
// TODO - implement
const updateAvatarMutation = useMutation({
mutationFn: async (_avatar: File | undefined) => {
// Fake loading
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Updating account image to", _avatar);
throw new Error("Not implemented");
},
});
function handleSave() {
const promises = updateAvatarMutation.mutateAsync(avatar);
toast.promise(promises, {
success: "Account avatar updated successfully",
error: "Failed to update account avatar",
});
}
return (
<SettingsCard
bottomText="An avatar is optional but strongly recommended."
saveButton={{
onClick: handleSave,
disabled: false,
isLoading: updateAvatarMutation.isLoading,
}}
noPermissionText={undefined}
errorText={undefined}
>
<div className="flex flex-row gap-4 md:justify-between">
<div>
<h3 className="text-xl font-semibold tracking-tight">Avatar</h3>
<p className="text-foreground text-sm mt-1.5 mb-4 leading-relaxed">
This is your account's avatar. <br /> Click on the avatar to upload
a custom one
</p>
</div>
<FileInput
accept={{ "image/*": [] }}
value={avatar}
setValue={setAvatar}
className="w-20 lg:w-28 rounded-full"
disableHelperText
/>
</div>
</SettingsCard>
);
}
function AccountNameFormControl(props: {
name: string;
}) {
const [accountName, setAccountName] = useState(props.name);
const maxAccountNameLength = 32;
// TODO - implement
const updateAccountNameMutation = useMutation({
mutationFn: async (name: string) => {
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Updating account name to", name);
throw new Error("Not implemented");
},
});
function handleSave() {
const promises = updateAccountNameMutation.mutateAsync(accountName);
toast.promise(promises, {
success: "Account name updated successfully",
error: "Failed to update account name",
});
}
return (
<SettingsCard
header={{
title: "Display Name",
description: "This is your account's name displayed on thirdweb",
}}
bottomText={`Please use ${maxAccountNameLength} characters at maximum.`}
saveButton={{
onClick: handleSave,
disabled: accountName.length === 0,
isLoading: updateAccountNameMutation.isLoading,
}}
errorText={undefined}
noPermissionText={undefined} // TODO
>
<Input
value={accountName}
onChange={(e) => {
setAccountName(e.target.value.slice(0, maxAccountNameLength));
}}
className="md:w-[400px]"
/>
</SettingsCard>
);
}
function DeleteAccountCard() {
const router = useDashboardRouter();
const title = "Delete Account";
const description =
"Permanently remove your Personal Account and all of its contents from the thirdweb platform. This action is not reversible, please continue with caution.";
// TODO
const deleteAccount = useMutation({
mutationFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Deleting account");
throw new Error("Not implemented");
},
onSuccess: () => {
router.push("/team");
},
});
function handleDelete() {
const promises = deleteAccount.mutateAsync();
toast.promise(promises, {
success: "Account deleted successfully",
error: "Failed to delete account",
});
}
return (
<DangerSettingCard
title={title}
description={description}
buttonLabel={title}
buttonOnClick={handleDelete}
isLoading={deleteAccount.isLoading}
confirmationDialog={{
title: "Are you sure you want to delete your account?",
description:
"This action is not reversible and will delete all of your data.",
}}
/>
);
}
function AccountEmailFormControl(props: {
email: string;
status: "unverified" | "verfication-sent" | "verified";
}) {
// TODO - query for account changes when the email is updated
return (
<SettingsCard
header={{
title: "Email",
description:
"Enter the email address you want to use to log in with thirdweb. This email will be used for account related notifications",
}}
bottomText={
"Emails must be verified to be able to login with them or be used as primary email"
}
errorText={undefined}
noPermissionText={undefined}
>
<div className="flex gap-4 justify-between items-center border rounded-lg p-4 bg-card">
{/* Start */}
<div className="flex flex-col lg:flex-row lg:items-center items-start gap-2 lg:gap-3">
<p className="text-sm"> {props.email}</p>
<Badge
className="capitalize "
variant={props.status === "unverified" ? "outline" : "default"}
>
{props.status}
</Badge>
</div>
{/* End */}
<EmailUpdateDialog
currentEmail={props.email}
trigger={
<Button
size="icon"
variant="ghost"
className="!h-auto !w-auto p-1.5"
>
<EllipsisIcon className="size-5 text-muted-foreground" />
</Button>
}
/>
</div>
</SettingsCard>
);
}
const emailUpdateFormSchema = z.object({
email: z.string().min(1, "Email can not be empty").max(100),
});
function EmailUpdateDialog(props: {
currentEmail: string;
trigger: React.ReactNode;
}) {
const form = useForm<z.infer<typeof emailUpdateFormSchema>>({
resolver: zodResolver(emailUpdateFormSchema),
values: {
email: props.currentEmail,
},
});
// TODO - implement
const updateEmailMutation = useMutation({
mutationFn: async (_email: string) => {
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("Updating account email to", _email);
throw new Error("Not implemented");
},
});
function onSubmit(values: z.infer<typeof emailUpdateFormSchema>) {
const promises = updateEmailMutation.mutateAsync(values.email);
toast.promise(promises, {
success: "Email updated successfully",
error: "Failed to update email",
});
}
return (
<Dialog>
<DialogTrigger asChild>{props.trigger}</DialogTrigger>
<DialogContent className="p-0">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="p-6 flex flex-col gap-4 pb-10">
<DialogHeader className="pr-10">
<DialogTitle className="text-2xl">Update Email</DialogTitle>
<DialogDescription>
A confirmation email will be sent to verify email address
</DialogDescription>
</DialogHeader>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
{...field}
className="selection:bg-foreground/20"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<DialogFooter className="gap-4 lg:gap-1 bg-muted/50 p-6 border-t">
<DialogClose asChild>
<Button variant="outline"> Cancel </Button>
</DialogClose>
<Button className="min-w-24 gap-2" type="submit">
{updateEmailMutation.isLoading && (
<Spinner className="size-4" />
)}
{updateEmailMutation.isLoading ? "Saving" : "Save"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,27 @@
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import { getAuthToken } from "../../api/lib/getAuthToken";
export async function getAccount() {
const authToken = getAuthToken();
const apiServerURL = new URL(
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
);
apiServerURL.pathname = "/v1/account/me";
const res = await fetch(apiServerURL, {
method: "GET",
headers: {
Authorization: `Bearer ${authToken}`,
},
});
const json = await res.json();
if (json.error) {
console.error(json.error);
return undefined;
}
return json.data as Account;
}
@@ -0,0 +1,13 @@
import { redirect } from "next/navigation";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
import { getAccount } from "./getAccount";
export default async function Page() {
const account = await getAccount();
if (!account) {
redirect(`/login?next=${encodeURIComponent("/account")}`);
}
return <AccountSettingsPageUI account={account} />;
}
@@ -0,0 +1,94 @@
"use client";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Button } from "@/components/ui/button";
import { MinusIcon, PlusIcon } from "lucide-react";
import { useState } from "react";
import { SearchInput } from "../components/SearchInput";
export function LinkWalletUI(props: {
wallets: string[];
}) {
const [searchValue, setSerchValue] = useState("");
const walletsToShow = !searchValue
? props.wallets
: props.wallets.filter((v) => {
return v.toLowerCase().includes(searchValue.toLowerCase());
});
return (
<div>
<div className="flex items-start gap-4 flex-col lg:flex-row lg:justify-between">
<div>
<h2 className="font-semibold text-xl tracking-tight mb-0.5">
Linked Wallets
</h2>
<p className="text-muted-foreground text-sm">
The wallets that are linked to your thirdweb account
</p>
</div>
{/* TODO - handle linking */}
<Button variant="primary" className="max-sm:w-full gap-2">
<PlusIcon className="size-4" />
Link a Wallet
</Button>
</div>
<div className="h-4" />
<SearchInput
placeholder="Search wallet address"
value={searchValue}
onValueChange={setSerchValue}
/>
<div className="h-4" />
<ul className="bg-muted/50 border rounded-lg">
{walletsToShow.map((v) => {
return (
<li key={v} className="border-b border-border p-4 last:border-b-0">
<WalletRow address={v} />
</li>
);
})}
{/* No Result Found */}
{walletsToShow.length === 0 && (
<div className="p-4 h-[200px] justify-center flex items-center">
<div className="text-center flex flex-col gap-3">
<p className="text-sm">No Wallets Found</p>
{searchValue && (
<p className="text-sm text-muted-foreground">
Your search for {`"${searchValue}"`} did not match any wallets
</p>
)}
</div>
</div>
)}
</ul>
</div>
);
}
function WalletRow(props: {
address: string;
}) {
return (
<div className="flex gap-2 items-center justify-between">
{/* start */}
<WalletAddress
address={props.address}
className="text-muted-foreground"
/>
{/* end */}
{/* TODO - handle unlink */}
<Button size="sm" variant="outline" className="gap-2">
<MinusIcon className="size-4" />
Unlink
</Button>
</div>
);
}
@@ -0,0 +1,62 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ThirdwebProvider } from "thirdweb/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { LinkWalletUI } from "./LinkWalletUI";
const meta = {
title: "Account/Pages/Link Wallets",
component: Variants,
parameters: {
nextjs: {
appDirectory: true,
},
},
} satisfies Meta<typeof Variants>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
args: {
type: "desktop",
},
};
export const Mobile: Story = {
args: {
type: "mobile",
},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Variants() {
return (
<ThirdwebProvider>
<div className="container py-10 max-w-[1100px] mx-auto w-full flex flex-col gap-10">
<BadgeContainer label="4 wallets">
<LinkWalletUI
wallets={[
"0x51696930092b42243dee1077c8dd237074fb28d4", // jns.eth - ens
"0xd8da6bf26964af9d7eed9e03e53415d37aa96045", // vitalik.eth - ens, farcaster, lens
"0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37", // no ens
]}
/>
</BadgeContainer>
<BadgeContainer label="1 wallet">
<LinkWalletUI
wallets={[
"0x51696930092b42243dee1077c8dd237074fb28d4", // jns.eth
]}
/>
</BadgeContainer>
<BadgeContainer label="0 wallets">
<LinkWalletUI wallets={[]} />
</BadgeContainer>
</div>
</ThirdwebProvider>
);
}
@@ -0,0 +1,12 @@
import { LinkWalletUI } from "./LinkWalletUI";
export default async function Page() {
return (
<div className="grow">
<LinkWalletUI
// TODO - fetch the wallets of current user
wallets={[]}
/>
</div>
);
}
@@ -0,0 +1,27 @@
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
import { getAuthToken } from "./getAuthToken";
export async function getAPIKey(apiKeyId: string) {
const authToken = getAuthToken();
const apiServerURL = new URL(
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com",
);
apiServerURL.pathname = `/v1/keys/${apiKeyId}`;
const res = await fetch(apiServerURL, {
method: "GET",
headers: {
Authorization: `Bearer ${authToken}`,
},
});
const json = await res.json();
if (json.error) {
console.error(json.error);
return undefined;
}
return json.data as ApiKey;
}
@@ -0,0 +1,12 @@
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
import { cookies } from "next/headers";
export function getAuthToken() {
const cookiesManager = cookies();
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
const token = activeAccount
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
: null;
return token;
}
@@ -8,6 +8,9 @@ const NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET =
process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET;
const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN;
// Comma-separated list of chain IDs to disable faucet for.
const DISABLE_FAUCET_CHAIN_IDS = process.env.DISABLE_FAUCET_CHAIN_IDS;
// Note: This handler cannot use "edge" runtime because of Redis usage.
export const GET = async (req: NextRequest) => {
const searchParams = req.nextUrl.searchParams;
@@ -33,10 +36,22 @@ export const GET = async (req: NextRequest) => {
);
}
// Check if faucet is disabled for this chain.
let isFaucetDisabled = false;
if (DISABLE_FAUCET_CHAIN_IDS) {
try {
const disableFaucetChainIds = DISABLE_FAUCET_CHAIN_IDS.split(",").map(
(chainId) => Number.parseInt(chainId.trim()),
);
isFaucetDisabled = disableFaucetChainIds.includes(chainId);
} catch {}
}
if (
!THIRDWEB_ENGINE_URL ||
!NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET ||
!THIRDWEB_ACCESS_TOKEN
!THIRDWEB_ACCESS_TOKEN ||
isFaucetDisabled
) {
const res: CanClaimResponseType = {
canClaim: false,
@@ -45,12 +60,15 @@ export const GET = async (req: NextRequest) => {
return NextResponse.json(res);
}
// vercel provides this for us in the request object || fall back to the header
const ipAddress = req.ip || req.headers.get("X-Forwarded-For");
// CF header, fallback to req.ip, then X-Forwarded-For
const ipAddress =
req.headers.get("CF-Connecting-IP") ||
req.ip ||
req.headers.get("X-Forwarded-For");
if (!ipAddress) {
return NextResponse.json(
{
error: "Could not validate elligibility.",
error: "Could not validate eligibility.",
},
{ status: 400 },
);
@@ -1,7 +1,13 @@
import { startOfToday } from "date-fns";
import { cacheGet, cacheSet } from "lib/redis";
import { type NextRequest, NextResponse } from "next/server";
import { ZERO_ADDRESS } from "thirdweb";
const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL;
const NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET =
process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET;
const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN;
interface RequestTestnetFundsPayload {
chainId: number;
toAddress: string;
@@ -15,11 +21,6 @@ export const POST = async (req: NextRequest) => {
throw new Error("Invalid chain ID.");
}
const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL;
const NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET =
process.env.NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET;
const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN;
if (
!THIRDWEB_ENGINE_URL ||
!NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET ||
@@ -31,8 +32,11 @@ export const POST = async (req: NextRequest) => {
);
}
// vercel provides this for us in the request object || fall back to the header
const ipAddress = req.ip || req.headers.get("X-Forwarded-For");
// CF header, fallback to req.ip, then X-Forwarded-For
const ipAddress =
req.headers.get("CF-Connecting-IP") ||
req.ip ||
req.headers.get("X-Forwarded-For");
if (!ipAddress) {
return NextResponse.json(
{
@@ -42,28 +46,44 @@ export const POST = async (req: NextRequest) => {
);
}
const cacheKey = `testnet-faucet:${chainId}:${ipAddress}`;
const ipCacheKey = `testnet-faucet:${chainId}:${ipAddress}`;
const addressCacheKey = `testnet-faucet:${chainId}:${toAddress}`;
// Assert 1 request per IP/chain every 24 hours.
// get the cached value
const cachedValue = await cacheGet(cacheKey);
const [ipCacheValue, addressCache] = await Promise.all([
cacheGet(ipCacheKey),
cacheGet(addressCacheKey),
]);
// if we have a cached value, return an error
if (cachedValue !== null) {
if (ipCacheValue !== null || addressCache !== null) {
return NextResponse.json(
{ error: "Already requested funds on this chain in the past 24 hours." },
{ status: 429 },
);
}
// Set an idempotencyKey to prevent duplicate claims for this chain/ip/time period.
const todayLocal = startOfToday();
const todayUTC = new Date(
todayLocal.getTime() - todayLocal.getTimezoneOffset() * 60000,
);
const todayUTCSeconds = Math.floor(todayUTC.getTime() / 1000);
const idempotencyKey = `${ipCacheKey}:${todayUTCSeconds}`;
try {
// Store the claim request for 24 hours.
await cacheSet(cacheKey, "claimed", 24 * 60 * 60);
await Promise.all([
cacheSet(ipCacheKey, "claimed", 24 * 60 * 60),
cacheSet(addressCacheKey, "claimed", 24 * 60 * 60),
]);
// then actually transfer the funds
const url = `${THIRDWEB_ENGINE_URL}/backend-wallet/${chainId}/transfer`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-idempotency-key": idempotencyKey,
"x-backend-wallet-address": NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
Authorization: `Bearer ${THIRDWEB_ACCESS_TOKEN}`,
},
@@ -1,15 +1,16 @@
"use client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { CmdKSearchModal } from "components/cmd-k-search";
import { ChevronDownIcon, CommandIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { Button } from "../../../../@/components/ui/button";
import { CmdKSearchModal } from "../../../../components/cmd-k-search";
export function ResourcesDropdownButton() {
const [isCMDSearchModalOpen, setIsCMDSearchModalOpen] = useState(false);
@@ -29,48 +30,47 @@ export function ResourcesDropdownButton() {
<ChevronDownIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-4 min-w-[230px]" sideOffset={14}>
<div className="flex flex-col gap-4">
<Link
href="/chainlist"
className="text-muted-foreground hover:text-foreground text-sm flex items-center gap-2"
>
<DropdownMenuContent className="min-w-[230px]" sideOffset={14}>
<DropdownMenuItem>
<Link href="/chainlist" className="w-full p-1">
Chainlist
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Link
href="https://playground.thirdweb.com/"
target="_blank"
className="text-muted-foreground hover:text-foreground text-sm flex items-center gap-2"
className="w-full p-1"
>
Playground
</Link>
</DropdownMenuItem>
<Link
href="/explore"
className="text-muted-foreground hover:text-foreground text-sm"
>
<DropdownMenuItem>
<Link href="/explore" className="w-full p-1">
Explore Contracts
</Link>
</DropdownMenuItem>
<Link
href="/trending"
className="text-muted-foreground hover:text-foreground text-sm"
>
<DropdownMenuItem>
<Link href="/trending" className="w-full p-1">
Popular Contracts
</Link>
</DropdownMenuItem>
<DropdownMenuItem>
<Button
variant="link"
className="text-muted-foreground text-sm gap-2 !p-0 !h-auto hover:!no-underline hover:text-foreground text-left justify-between"
variant="ghost"
className="text-sm gap-2 !p-1 !h-auto text-left justify-between w-full"
onClick={() => setIsCMDSearchModalOpen(true)}
>
Search Contracts
<span className="items-center flex gap-0.5 text-xs opacity-80">
<span className="items-center flex gap-0.5 text-xs text-muted-foreground">
<CommandIcon className="size-3" />K
</span>
</Button>
</div>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
@@ -0,0 +1,36 @@
import Link from "next/link";
import type React from "react";
import { ResourcesDropdownButton } from "./ResourcesDropdownButton";
import { AccountButton } from "./account-button.client";
export function SecondaryNav(props: {
email: string | undefined;
logout: () => void;
connectButton: React.ReactNode;
}) {
return (
<div className="flex items-center gap-6">
<ResourcesDropdownButton />
<Link
href="/support"
className="text-muted-foreground text-sm hover:text-foreground"
>
Support
</Link>
<Link
href="https://portal.thirdweb.com/"
className="text-muted-foreground text-sm hover:text-foreground"
>
Docs
</Link>
<AccountButton
email={props.email}
logout={props.logout}
connectButton={props.connectButton}
/>
</div>
);
}
@@ -1,17 +1,16 @@
"use client";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import { LogOutIcon, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import Link from "next/link";
import { cn } from "../../../../@/lib/utils";
export function AccountButton(props: {
email: string | undefined;
@@ -29,18 +28,19 @@ export function AccountButton(props: {
variant="ghost"
>
{/* TODO - replace with account image */}
<div className="size-9 rounded-full bg-muted border" />
<div className="size-9 rounded-full bg-muted border border-border" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
className="p-0 w-[300px] rounded-lg"
align="end"
sideOffset={16}
>
<div className="p-4 pb-5 border-b">
<p className="text-sm text-muted-foreground">{props.email}</p>
<DropdownMenuContent className="p-0 w-[300px] rounded-lg" align="end">
<div className="p-4 pb-5 border-b border-border">
<SkeletonContainer
skeletonData="[email protected]"
loadedData={props.email}
render={(v) => <p className="text-sm text-muted-foreground">{v}</p>}
/>
<div className="h-3" />
<div className="[&>button]:!w-full">{props.connectButton}</div>
<div className="[&>*]:!w-full">{props.connectButton}</div>
</div>
<div className="flex flex-col gap-4 px-4 py-5">
@@ -54,7 +54,7 @@ export function AccountButton(props: {
<div className="flex justify-between gap-2 items-center">
<p className="text-muted-foreground text-sm">Theme</p>
<div className="ml-auto flex items-center border px-2 py-0.5 rounded-lg gap-1">
<div className="ml-auto flex items-center border border-border px-2 py-0.5 rounded-lg gap-1">
<Button
size="icon"
onClick={() => setTheme("light")}
@@ -83,7 +83,7 @@ export function AccountButton(props: {
</div>
<Link
href="/team"
href="/"
className="text-muted-foreground text-sm hover:text-foreground"
>
Home Page
@@ -2,7 +2,7 @@ export const headerLinks = {
left: [
{
name: "Connect",
href: "/dashboard/connect",
href: "/dashboard/connect/analytics",
},
{
name: "Contracts",
@@ -2,6 +2,7 @@
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { cn } from "@/lib/utils";
import {
LogOutIcon,
@@ -14,8 +15,7 @@ import {
import { useTheme } from "next-themes";
import Link from "next/link";
import { useLayoutEffect, useState } from "react";
import { SkeletonContainer } from "../../../../@/components/ui/skeleton";
import { CmdKSearchModal } from "../../../../components/cmd-k-search";
import { CmdKSearchModal } from "../../components/cmd-k-search";
export function MobileBurgerMenuButton(props: {
email: string | undefined;
@@ -0,0 +1,22 @@
import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
export function TeamPlanBadge(props: {
plan: "free" | "growth" | "pro";
className?: string;
}) {
return (
<Badge
variant={
props.plan === "free"
? "secondary"
: props.plan === "growth"
? "success"
: "default"
}
className={cn("capitalize px-1.5", props.className)}
>
{props.plan}
</Badge>
);
}
@@ -71,7 +71,7 @@ export function TeamOverviewPage(props: {
{/* Projects */}
{projectsToShow.length === 0 ? (
<div className="border rounded-lg h-[450px] flex items-center justify-center ">
<div className="border border-border rounded-lg h-[450px] flex items-center justify-center ">
No projects found
</div>
) : (
@@ -101,7 +101,7 @@ function ProjectCard(props: {
return (
<div
key={project.id}
className="border rounded-lg p-4 relative hover:bg-muted/70 flex items-center gap-4 bg-muted/50 transition-colors"
className="border border-border rounded-lg p-4 relative hover:bg-muted/70 flex items-center gap-4 bg-muted/50 transition-colors"
>
{/* TODO - replace with project image */}
<div className="size-10 rounded-full bg-border shrink-0" />
@@ -1,12 +1,11 @@
import { getProjects } from "@/api/projects";
import { getTeams } from "@/api/team";
import { TabPathLinks } from "@/components/ui/tabs";
import { redirect } from "next/navigation";
import { getProjects } from "../../../../@/api/projects";
import { TeamHeader } from "../../../components/Header/TeamHeader/TeamHeader";
import TeamTabs from "../components/tab-switcher.client";
import { TeamHeader } from "../../components/TeamHeader/TeamHeader";
export default async function TeamLayout(props: {
children: React.ReactNode;
breadcrumbNav: React.ReactNode;
params: { team_slug: string };
}) {
const teams = await getTeams();
@@ -32,19 +31,20 @@ export default async function TeamLayout(props: {
currentProject={undefined}
/>
<TeamTabs
<TabPathLinks
tabContainerClassName="px-4 lg:px-6"
links={[
{
href: `/team/${props.params.team_slug}`,
path: `/team/${props.params.team_slug}`,
name: "Projects",
strictMatch: true,
exactMatch: true,
},
{
href: `/team/${props.params.team_slug}/~/usage`,
path: `/team/${props.params.team_slug}/~/usage`,
name: "Usage",
},
{
href: `/team/${props.params.team_slug}/~/settings`,
path: `/team/${props.params.team_slug}/~/settings`,
name: "Settings",
},
]}
@@ -29,7 +29,7 @@ function RenderLinkGroup(props: {
<>
<div className="flex items-center gap-1.5 px-4">
{/* TODO - using placeholder for now */}
<div className="size-4 bg-muted rounded-full border" />
<div className="size-4 bg-muted rounded-full border border-border" />
<p className="text-muted-foreground text-xs">{props.title}</p>
</div>
@@ -13,7 +13,7 @@ export function TeamSettingsMobileNav(props: {
if (!showFull) {
return (
<div className="py-4 px-4 border-b flex items-center gap-2 text-muted-foreground">
<div className="py-4 px-4 border-b border-border flex items-center gap-2 text-muted-foreground">
<Link
href={teamLinks[0].href}
className="inline-flex items-center gap-1"
@@ -43,7 +43,7 @@ export function TeamSettingsMobileNav(props: {
<li key={link.href}>
<Link
href={link.href}
className="p-4 border-b flex"
className="p-4 border-b flex border-border"
onClick={() => {
if (link === teamLinks[0]) {
setShowFull(false);
@@ -0,0 +1,29 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { AccountStatus, useAccount } from "@3rdweb-sdk/react/hooks/useApi";
import { Billing } from "components/settings/Account/Billing";
export const SettingsBillingPage = () => {
const meQuery = useAccount({
refetchInterval: (account) =>
[
AccountStatus.InvalidPayment,
AccountStatus.InvalidPaymentMethod,
].includes(account?.status as AccountStatus)
? 1000
: false,
});
const { data: account } = meQuery;
if (!account) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Spinner className="size-10" />
</div>
);
}
return <Billing account={account} />;
};
@@ -1,9 +1,10 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { SettingsBillingPage } from "./BillingSettingsPage";
export default function Page() {
return (
<div className="h-full py-6 container flex items-center justify-center">
<h1 className="text-4xl tracking-tighter text-muted-foreground">
Billing Settings
</h1>
</div>
<ChakraProviderSetup>
<SettingsBillingPage />
</ChakraProviderSetup>
);
}
@@ -0,0 +1,40 @@
"use client";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
import { Flex } from "@chakra-ui/react";
import { ApplyForOpCreditsModal } from "components/onboarding/ApplyForOpCreditsModal";
import { Heading, LinkButton } from "tw-components";
export const SettingsGasCreditsPage = () => {
const { isLoading } = useLoggedInUser();
if (isLoading) {
return (
<div className="grid w-full min-h-[400px] place-items-center">
<Spinner className="size-10" />
</div>
);
}
return (
<Flex flexDir="column" gap={8}>
<Flex direction="row" gap={4} align="center">
<Heading size="title.lg" as="h1">
Apply to the Optimism Superchain App Accelerator
</Heading>
<LinkButton
display={{ base: "none", md: "inherit" }}
isExternal
href="https://blog.thirdweb.com/accelerating-the-superchain-with-optimism/"
size="sm"
variant="outline"
>
Learn More
</LinkButton>
</Flex>
<ApplyForOpCreditsModal />
</Flex>
);
};
@@ -1,9 +1,10 @@
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
import { SettingsGasCreditsPage } from "./SettingsCreditsPage";
export default function Page() {
return (
<div className="h-full py-6 container flex items-center justify-center">
<h1 className="text-4xl tracking-tighter text-muted-foreground">
Credits Settings
</h1>
</div>
<ChakraProviderSetup>
<SettingsGasCreditsPage />
</ChakraProviderSetup>
);
}
@@ -40,6 +40,10 @@ function Story() {
buttonLabel="Some Action"
buttonOnClick={() => {}}
isLoading={false}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
}}
/>
</BadgeContainer>
@@ -50,6 +54,10 @@ function Story() {
buttonLabel="Some Action"
buttonOnClick={() => {}}
isLoading={true}
confirmationDialog={{
title: "This is confirmation title",
description: "This is confirmation description",
}}
/>
</BadgeContainer>
</div>
@@ -1,12 +1,27 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function DangerSettingCard(props: {
title: string;
description: string;
buttonLabel: string;
buttonOnClick: () => void;
isLoading: boolean;
confirmationDialog: {
title: string;
description: string;
};
}) {
return (
<div className="border border-red-500/70 rounded-lg overflow-hidden">
@@ -19,14 +34,46 @@ export function DangerSettingCard(props: {
</div>
<div className="bg-red-100 dark:bg-red-500/20 px-4 lg:px-6 py-4 flex justify-end border-red-500/70 border-t">
<Button
variant="destructive"
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
onClick={props.buttonOnClick}
>
{props.isLoading && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
<Dialog>
<DialogTrigger asChild>
<Button
variant="destructive"
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
disabled={props.isLoading}
>
{props.isLoading && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader className="pr-10">
<DialogTitle className="leading-snug">
{props.confirmationDialog.title}
</DialogTitle>
<DialogDescription>
{props.confirmationDialog.description}
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-4 gap-4 lg:gap-2">
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button
variant="destructive"
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
onClick={props.buttonOnClick}
disabled={props.isLoading}
>
{props.isLoading && <Spinner className="size-3" />}
{props.buttonLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
);
@@ -65,14 +65,14 @@ function Story() {
function ComponentVariantions() {
return (
<div className="pt-20 border-t p-6">
<div className="pt-20 border-t p-6 border-border">
<div className="max-w-[1100px] mx-auto">
<h2 className="mb-5 text-3xl"> Component variations </h2>
<div className="flex flex-col gap-6">
<LeaveTeamCard enabled={true} />
<LeaveTeamCard enabled={false} />
<DeleteTeamCard enabled={true} />
<DeleteTeamCard enabled={false} />
<LeaveTeamCard enabled={true} teamName="foo" />
<LeaveTeamCard enabled={false} teamName="foo" />
<DeleteTeamCard enabled={true} teamName="foo" />
<DeleteTeamCard enabled={false} teamName="foo" />
</div>
</div>
</div>
@@ -3,6 +3,7 @@
import type { Team } from "@/api/team";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Input } from "@/components/ui/input";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useMutation } from "@tanstack/react-query";
import { FileInput } from "components/shared/FileInput";
import { useState } from "react";
@@ -20,8 +21,11 @@ export function GeneralSettingsPage(props: {
<TeamSlugFormControl team={props.team} />
<TeamAvatarFormControl />
<TeamIdCard team={props.team} />
<LeaveTeamCard enabled={false} />
<DeleteTeamCard enabled={hasPermissionToDelete} />
<LeaveTeamCard enabled={false} teamName={props.team.name} />
<DeleteTeamCard
enabled={hasPermissionToDelete}
teamName={props.team.name}
/>
</div>
);
}
@@ -121,8 +125,8 @@ function TeamSlugFormControl(props: {
}}
noPermissionText={undefined} // TODO
>
<div className="relative border rounded-lg flex md:w-[450px]">
<div className="bg-muted/50 text-muted-foreground/80 self-stretch flex items-center px-3 text-sm border-r rounded-l-lg font-mono">
<div className="relative border border-border rounded-lg flex md:w-[450px]">
<div className="bg-muted/50 text-muted-foreground/80 self-stretch flex items-center px-3 text-sm border-r border-border rounded-l-lg font-mono">
thirdweb.com/team/
</div>
<Input
@@ -181,7 +185,7 @@ function TeamAvatarFormControl() {
accept={{ "image/*": [] }}
value={teamAvatar}
setValue={setTeamAvatar}
className="w-[120px] rounded-full"
className="w-20 lg:w-28 rounded-full"
disableHelperText
/>
</div>
@@ -216,6 +220,7 @@ function TeamIdCard(props: {
export function LeaveTeamCard(props: {
enabled: boolean;
teamName: string;
}) {
const title = "Leave Team";
const description =
@@ -246,6 +251,11 @@ export function LeaveTeamCard(props: {
buttonLabel={title}
buttonOnClick={handleLeave}
isLoading={leaveTeam.isLoading}
confirmationDialog={{
title: `Are you sure you want to leave team "${props.teamName}" ?`,
description:
"This will revoke your access to this Team. Any resources you've added to the Team will remain.",
}}
/>
);
}
@@ -267,7 +277,9 @@ export function LeaveTeamCard(props: {
export function DeleteTeamCard(props: {
enabled: boolean;
teamName: string;
}) {
const router = useDashboardRouter();
const title = "Delete Team";
const description =
"Permanently remove your team and all of its contents from the thirdweb platform. This action is not reversible - please continue with caution.";
@@ -279,6 +291,9 @@ export function DeleteTeamCard(props: {
console.log("Deleting team");
throw new Error("Not implemented");
},
onSuccess: () => {
router.push("/team");
},
});
function handleDelete() {
@@ -297,6 +312,10 @@ export function DeleteTeamCard(props: {
buttonLabel={title}
buttonOnClick={handleDelete}
isLoading={deleteTeam.isLoading}
confirmationDialog={{
title: `Are you sure you want to delete team "${props.teamName}" ?`,
description: description,
}}
/>
);
}
@@ -1,6 +1,6 @@
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { cn } from "../../../../../../../@/lib/utils";
import { cn } from "@/lib/utils";
export function SettingsCard(props: {
bottomText: string;
@@ -21,7 +21,7 @@ export function SettingsCard(props: {
<div className="border rounded-lg bg-muted/50 relative">
<div
className={cn(
"border-b px-4 lg:px-6 py-6 relative",
"border-b border-border px-4 lg:px-6 py-6 relative",
props.noPermissionText && "cursor-not-allowed",
)}
>
@@ -38,9 +38,7 @@ function Story(props: {
type: "mobile" | "desktop";
}) {
return (
<div
className={"min-h-screen text-foreground lg:max-w-[1100px] py-6 mx-auto"}
>
<div className={"min-h-screen text-foreground container py-6"}>
{props.type === "desktop" && <TeamSettingsSidebar teamSlug="foo" />}
{props.type === "mobile" && (
@@ -27,8 +27,8 @@ export default function Layout(props: {
return (
<div>
{/* Huge page title */}
<div className="py-10 border-b">
<div className="container max-w-[1200px]">
<div className="py-10 border-b border-border">
<div className="container">
<h1 className="text-3xl tracking-tight font-semibold">
Team Settings
</h1>
@@ -44,7 +44,7 @@ export default function Layout(props: {
/>
</div>
<div className="flex gap-8 container max-w-[1200px] grow [&>*]:py-8 lg:[&>*]:py-10 lg:min-h-[900px]">
<div className="flex gap-8 container grow [&>*]:py-8 lg:[&>*]:py-10 lg:min-h-[900px]">
<TeamSettingsSidebar teamSlug={props.params.team_slug} />
<div
className={cn(
@@ -17,7 +17,7 @@ import { cn } from "@/lib/utils";
import { ExternalLinkIcon, LinkIcon, UserPlus } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { getValidTeamPlan } from "../../../../../../components/Header/TeamHeader/getValidTeamPlan";
import { getValidTeamPlan } from "../../../../../components/TeamHeader/getValidTeamPlan";
export function InviteSection(props: {
team: Team;
@@ -29,7 +29,7 @@ export function InviteSection(props: {
if (teamPlan !== "pro") {
bottomSection = (
<div className="px-4 lg:px6 py-4 flex items-center justify-between border-t gap-4">
<div className="px-4 lg:px6 py-4 flex items-center justify-between border-t border-border gap-4">
<p className="text-muted-foreground text-sm">
This feature is only available on the{" "}
<Link
@@ -49,7 +49,7 @@ export function InviteSection(props: {
);
} else if (!props.userHasEditPermission) {
bottomSection = (
<div className="px-4 lg:px-6 py-4 flex items-center justify-between border-t min-h-[60px]">
<div className="px-4 lg:px-6 py-4 flex items-center justify-between border-t border-border min-h-[60px]">
<p className="text-muted-foreground text-sm">
You don't have permission to invite members
</p>
@@ -57,7 +57,7 @@ export function InviteSection(props: {
);
} else {
bottomSection = (
<div className="py-4 px-4 lg:px-6 flex items-center lg:justify-end border-t ">
<div className="py-4 px-4 lg:px-6 flex items-center lg:justify-end border-t border-border">
<Button variant="outline" size="sm" className="gap-2 max-sm:w-full">
<UserPlus className="size-3" />
Invite
@@ -71,7 +71,7 @@ export function InviteSection(props: {
<h2 className="text-2xl font-semibold tracking-tight mb-3">Invite</h2>
{/* Card */}
<div className="border rounded-lg bg-muted/50">
<div className="border border-border rounded-lg bg-muted/50">
{/* Invite via Link */}
<div
className={cn(

Some files were not shown because too many files have changed in this diff Show More