Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
f618e2bffb Version Packages (#6705)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-11 22:40:10 +02:00
Jonas DanielsandGitHub 3ab31c8210 Fix incrBy to incrby in rate limit implementation (#6704) 2025-04-11 22:38:34 +02:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
8eb5d8c01e Version Packages (#6703)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-11 22:30:14 +02:00
Jonas DanielsandGitHub d036b87ae1 Refactor rate limiting to use separate read/write Redis operations (#6702) 2025-04-11 22:26:11 +02:00
YashandGitHub c86cac4d61 Fix getClaimConditions (#6701) 2025-04-11 19:19:32 +02:00
MananTank af09576247 Dashboard: Fix contract overpage page overflow when there are too many events in single tx (#6700)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `ContractOverviewPage` and `LatestEvents` components in a React application by adding overflow handling and improving the display of event data.

### Detailed summary
- Added `overflow-hidden` class to the `div` in `ContractOverviewPage.tsx`.
- Refactored the mapping of `allEvents` in `LatestEvents.tsx` to use a block structure for better readability.
- Limited the displayed events to 3 and added a button to show additional events if more than 3 exist.

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

<!-- end pr-codex -->
2025-04-11 17:01:47 +00:00
Manan TankandGitHub 316cef1a58 [TOOL-4057] Dashboard: Update Storage pinning card in team usage page (#6697) 2025-04-11 18:08:19 +02:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1bfcd375e4 Version Packages (#6696)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 20:51:36 -07:00
Joaquim VergesandGitHub 86070fa309 [Pay] Add feePayer option for direct_payment mode in PayEmbed (#6695) 2025-04-10 20:43:43 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
c5bf5a8594 Version Packages (#6694)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 14:16:49 -07:00
Joaquim VergesandGitHub c1d6bb7848 [Pay] Add extra gas buffer for quote and transfer transactions (#6693) 2025-04-10 14:10:45 -07:00
Toomas OosaluandGitHub e9db59e8da update insight plaground routes (#6692) 2025-04-10 12:55:59 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1db216545a Version Packages (#6691)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-10 12:55:22 -07:00
Joaquim VergesandGitHub 0a798c58b3 [SDK] Add smartAccountAddress to getUser result (#6690) 2025-04-10 12:17:31 -07:00
Joe-ThirdwebandGitHub cfacfc28a8 [Dashboard] Fix : Adding COTI chain page details (#6689)
Signed-off-by: Joe-Thirdweb <[email protected]>
2025-04-10 19:20:31 +02:00
KimaniandGitHub 2d978aea35 [Portal] Fix: updates broken-links on nebula sessions docs (#6687) 2025-04-10 19:17:19 +02:00
MananTank ca382f442a Dashboard: Remove certain warning logs (#6681)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating package dependencies and configuration settings for the project, including the addition of `prettier` and adjustments to `@aws-sdk/client-sso-oidc` references in the `pnpm-lock.yaml` file.

### Detailed summary
- Added `public-hoist-pattern` entries in `.npmrc`.
- Updated `next.config.ts` to modify `config.externals`.
- Added `prettier` dependency in `package.json`.
- Updated `pnpm-lock.yaml` to reflect the new `prettier` version.
- Cleaned up references to `@aws-sdk/client-sso-oidc` in `pnpm-lock.yaml`.

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

<!-- end pr-codex -->
2025-04-10 12:59:07 +00:00
0xFirekeeper 840b1a264f [Portal] Hotfix Unity sidebar link (#6688)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates the sidebar navigation in the `sidebar.tsx` file, changing the name and link for a specific menu item.

### Detailed summary
- Changed the menu item name from `"Payments"` to `"Universal Bridge"`.
- Updated the `href` from `"/dotnet/pay/quickstart"` to `"/dotnet/universal-bridge/quickstart"`.

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

<!-- end pr-codex -->
2025-04-10 12:42:27 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d803d37879 Version Packages (#6656)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-09 16:26:39 -07:00
Joaquim VergesandGitHub e10c1e696e [SDK] Pass through desired chain when signing in with wallet for ecosystems (#6686) 2025-04-09 16:03:43 -07:00
kumaryash90 a2305329fd Update deploy config (#6667)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces support for contracts that have no fees by adding new properties and constants to handle fee exemption logic. It modifies several components to utilize the new metadata and fee configurations, ensuring that contracts can be deployed without fees based on specific versions and chains.

### Detailed summary
- Added `contractMetadataNoFee` prop to `DeployFormForUri` and `CustomContractForm`.
- Introduced `ZERO_FEE_VERSIONS` and `ZERO_FEE_CHAINS` constants in `fee-config.ts`.
- Updated `DeployFormForPublishInfo` to fetch metadata for contracts without fees.
- Adjusted fee logic in `CustomContractForm` to use `ZERO_FEE_CHAINS`.
- Modified deployment logic to conditionally use fee-exempt metadata.

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

<!-- end pr-codex -->
2025-04-09 22:24:46 +00:00
MananTank 60235ade34 [TOOL-4028] Dashboard: Limit team name to 32 chars (#6685)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a maximum length restriction for the `teamName` input field in the `TeamGeneralSettingsPageUI` component and applies this restriction to the `TeamInfoFormUI` component as well, enhancing user experience by preventing excessively long team names.

### Detailed summary
- Added `maxLength` attribute with a value of `32` to the `teamName` input in `TeamGeneralSettingsPageUI`.
- Updated the `onChange` handler in `TeamGeneralSettingsPageUI` to set the `teamName` directly from the input value.
- Introduced a new constant `maxTeamNameLength` in `TeamInfoForm.tsx` set to `32`.
- Added `maxLength={maxTeamNameLength}` to the input field in `TeamInfoFormUI`.

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

<!-- end pr-codex -->
2025-04-09 21:19:40 +00:00
MananTank 811384b91f [TOOL-4029] Dashboard: Remove create engine page, Various UI udpates in engine instances page (#6684) 2025-04-09 20:57:36 +00:00
Jonas DanielsandGitHub 22dc4d7cd1 update dependencies (#6678) 2025-04-09 20:57:45 +02:00
gregfromstl 3074e7c67d Fix flaky ETH price test (#6682)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating tests in the `cryptoToFiat` and `Status` files, improving assertions and changing transaction details to reflect new data.

### Detailed summary
- In `cryptoToFiat.test.ts`:
  - Changed assertion from checking if `data.result` is greater than 1500 to ensuring it's greater than 0.

- In `Status.test.ts`:
  - Updated `transactionHash` and related transaction details, including:
    - Modified `destinationAmount`, `destinationChainId`, and `destinationTokenAddress`.
    - Adjusted `originAmount` and `originTokenAddress`.

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

<!-- end pr-codex -->
2025-04-09 17:48:09 +00:00
iuwqyir dd99410ccd update insight playground routes (#6680)
<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the `insightBlueprints` in the `insightBlueprints.ts` file by adding new paths for token and NFT lookups and modifying an existing path name for clarity.

### Detailed summary
- Added a new path for token lookup:
  - `name`: "Token lookup",
  - `path`: "/v1/tokens/lookup"
- Added a new path for getting NFT collections:
  - `name`: "Get collection",
  - `path`: "/v1/nfts/collections/{contract_address}"
- Changed the name of an existing path from "Get NFT by token" to "Get NFT by token ID".

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

<!-- end pr-codex -->
2025-04-09 12:33:24 +00:00
MananTank b4e5dc1f3c [Dashboard] Migrate missing extension overlay from chakra to tailwind css (#6677)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the UI components related to settings by adding `overflow="hidden"` to `Card` components and refining the `SettingDetectedState` component's structure and styling for improved clarity and responsiveness.

### Detailed summary
- Added `overflow="hidden"` to `Card` components in multiple files.
- Refactored `SettingDetectedState` to use a function declaration instead of a functional component type.
- Improved the layout and styling of the loading and error states in `SettingDetectedState`.
- Replaced `SimpleGrid` with a `div` for better control over layout.
- Updated text structure and classes for better styling consistency.

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

<!-- end pr-codex -->
2025-04-08 22:00:12 +00:00
MananTank 8b4091e848 [Dashboard] Contracts sources page: migrate from chakra to tailwind+shadcn (#6676)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR primarily focuses on enhancing the `SourcesPanel` and `SourcesAccordion` components by improving their structure and styling. It also introduces a new modal for contract verification and updates the UI elements for better user experience.

### Detailed summary
- Replaced `Text` and `Link` with `p` and `UnderlineLink` in `SourcesPanel`.
- Updated `SourcesAccordion` to use `SourceAccordionItem` for rendering items.
- Modified `VerifyContractModal` to a new functional component, `VerifyContractModalContent`.
- Enhanced loading and error handling in the verification process.
- Changed the layout and styling of the `ContractSourcesPage`.
- Introduced `Dialog` for contract verification instead of using a modal.
- Updated button styles and loading indicators for better UX.

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

<!-- end pr-codex -->
2025-04-08 21:34:47 +00:00
MananTank 2d15e6f509 Dashboard: Update pricing cards, style tweaks (#6675) 2025-04-08 18:41:28 +00:00
MananTank e450d320a0 [TOOL-3969] Dashboard: Update Plans, Add cancel plan, and other related UI updates (#6674)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the billing and subscription management system by adding new team plans, updating existing components, and improving the user interface for better user experience.

### Detailed summary
- Added new plans: `accelerate` and `scale`.
- Updated `TeamPlan` type to include new plans.
- Modified `PlanInfoCard` to support new plans and improved UI.
- Refactored billing components to use new plan data.
- Enhanced `InviteSection` with new team plan options.
- Updated storybook files for new billing plans.
- Improved `CancelPlanModal` for better user feedback.
- Added helper functions for plan CTA logic.

> The following files were skipped due to too many changes: `apps/dashboard/src/components/onboarding/ApplyForOpCreditsModal.tsx`

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

<!-- end pr-codex -->
2025-04-08 17:58:37 +00:00
MananTank 838f8fa547 Dashboard: Update contracts page header: render on top of sidebar (#6666) 2025-04-07 20:21:39 +00:00
MananTank 1dae74b9d2 Dashboard: Update NFT token details page styles, reduce chakra usage (#6665)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the UI components of the dashboard, enhancing the styling and structure of various elements, and adding new features like tooltips and breadcrumbs for better navigation.

### Detailed summary
- Changed `className` from `font-semibold` to `font-bold` in the `h1` element.
- Updated `NftProperty` component to use `div` instead of `Card`.
- Added `toolTip` property to `TabButtons`.
- Wrapped `Button` in `ToolTipLabel` for tooltips.
- Introduced `IPFSLinkGroup` component for displaying IPFS links.
- Enhanced `NFTName` and `NFTDescription` styling.
- Removed unnecessary imports and unused variables for cleaner code.

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

<!-- end pr-codex -->
2025-04-07 16:38:03 +00:00
MananTank 672f7d00bd Dashboard: Contract page style adjustments, fixes, reduce chakra usage (#6664)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the UI components within the dashboard, improving layout consistency, and updating text for better clarity and user experience.

### Detailed summary
- Updated `Button` styles in `primary-dashboard-button.tsx`.
- Increased gap in layout for `ContractOverviewPage.tsx`.
- Changed headings to `h2` for better accessibility in auction and listing pages.
- Introduced `StatCard` component for consistent stats display.
- Renamed `tokenBalanceQuery` to `ownedTokenBalanceQuery` for clarity.
- Updated `SupplyCards` to use `StatCard` instead of `Stat`.
- Improved layout structure with new divs and classes for better styling.
- Added `ArrowRightIcon` to navigation links for visual cues.

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

<!-- end pr-codex -->
2025-04-07 13:19:42 +00:00
MananTank a9eda9fc8d Dashboard: Migrate contract overview page components to tailwind/shadcn (#6662)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring and improving the UI components related to token and NFT details, enhancing the overall structure and user experience in the dashboard application.

### Detailed summary
- Removed unused files: `data.ts`, `types.ts`, and `TokenDetails.tsx`.
- Renamed `StatCard` to `Stat` for consistency.
- Updated UI components to improve layout and styling.
- Enhanced `TokenDetailsCard` and `NFTDetails` for better presentation.
- Added storybook entries for `PermissionsTable` and `LatestEvents`.
- Improved handling of loading states in various components.
- Replaced Chakra UI components with custom UI components for consistency.

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

<!-- end pr-codex -->
2025-04-07 09:23:59 +00:00
Manan TankandGitHub a3b4720f69 Dashboard: Remove chakra usage from contract analytics page, style tweaks (#6661) 2025-04-07 11:22:45 +02:00
Jonas DanielsandGitHub c2a5f65239 update dependencies (#6658) 2025-04-07 11:22:10 +02:00
Joaquim VergesandGitHub 7ab76ac55a [Dashboard] Add team ID to ecosystem API requests (#6659) 2025-04-06 17:04:50 -07:00
MananTank 386138e4f0 Dashboard: Team invoices page style tweaks (#6660)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the rendering of the invoice history page and improving the `BillingHistory` component's layout and functionality, including UI updates and pagination controls.

### Detailed summary
- Replaced `Card` components with a `div` structure for the invoice history.
- Updated headings and descriptions with new styling.
- Changed badge components for invoice statuses to use variants.
- Refined pagination controls with improved styling and layout.

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

<!-- end pr-codex -->
2025-04-05 13:53:12 +00:00
Reza RahemtolaandGitHub 309d1d544b [Docs] Fix: Add missing import (#6657)
Signed-off-by: Reza Rahemtola <[email protected]>
2025-04-05 10:17:51 +02:00
kumaryash90 59be4ea2fb [SDK/Dashboard] fix: ERC1155 single phase claim conditions (#6655)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on adding single phase functionality for `erc1155` claim conditions in the `thirdweb` dashboard and related utilities, enhancing the ability to manage claim eligibility and conditions for tokens.

### Detailed summary
- Introduced `isMultiphase` and `isSinglePhase` properties in various components.
- Updated `setClaimPhasesTx` to handle single phase options.
- Modified `getClaimConditions` to support both multi-phase and single-phase claims.
- Enhanced `resetClaimEligibility` to work with single phase drops.
- Updated tests to validate new single phase functionality.

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

<!-- end pr-codex -->
2025-04-05 02:26:03 +00:00
MananTank 9c737a1905 Insight Playground: Show error when no chains selected (#6654) 2025-04-04 19:59:22 +00:00
saminacodes 646a5a3788 Ub docs 2 (#6653)
CORE-0000
---

## PR-Codex overview
This PR focuses on updating the terminology and content related to crypto transactions and user identity features in the application. It enhances documentation and descriptions to better reflect the functionality of the components.

### Detailed summary
- Updated `description` in `PayEmbed` to "Prebuilt UI to buy crypto by swapping or fiat onramps".
- Changed title in `fund-wallets` from "Fund wallets" to "Buy Crypto".
- Added new `Details` sections in `faqs` for Universal Bridge operation and KYC requirements.
- Removed user identity links in `sidebar` and reorganized sections.

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

<!-- end pr-codex -->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the terminology and descriptions related to crypto purchases within the application, particularly changing references from "Fund wallets" to "Buy Crypto." It also adds new FAQ sections and modifies existing sidebar navigation.

### Detailed summary
- Updated `description` in `page.mdx` files to reflect "Buy Crypto" instead of "Fund wallets."
- Added new FAQ entries in `faqs/page.mdx` about Universal Bridge operations and KYC requirements.
- Changed sidebar navigation to include new entries and removed outdated user identity links.

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

<!-- end pr-codex -->
2025-04-04 17:55:24 +00:00
MananTank 7597418872 [TOOL-3564] Playground: Select multiple chains in insight playground (#6652)
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces a `MultiSelect` component along with a `useShowMore` hook to enhance the selection of network chains in a UI. It improves the user experience by allowing more items to be displayed as the user scrolls and provides a more flexible selection interface.

### Detailed summary
- Added `useShowMore` hook to manage displaying more items on scroll.
- Introduced `MultiNetworkSelector` for selecting multiple network chains.
- Updated `BlueprintPlayground` to utilize `MultiNetworkSelector`.
- Modified `ParameterSection` to handle array values for chain selection.
- Created `MultiSelect` component for better multi-item selection.
- Enhanced UI with search functionality and option rendering in `MultiSelect`.
- Implemented badge display for selected items in `MultiSelect`.

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

<!-- end pr-codex -->
2025-04-04 17:46:52 +00:00
MananTank 5204bd506f [TOOL-3954] Dashboard: Add tracking in contracts page (#6651) 2025-04-04 16:27:49 +00:00
MananTank 663627715a [TOOL-3876] Dashboard: Merge Framer sitemap.xml to dashboard sitemap.xml (#6648)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily introduces the `fast-xml-parser` package to the project and updates the `fetchChainsFromApi` function to fetch and parse XML data. It also modifies the sitemap generation logic to utilize the newly added functionality.

### Detailed summary
- Added `fast-xml-parser` to `package.json` and `pnpm-lock.yaml`.
- Updated `next-sitemap.config.js` to require `fast-xml-parser`.
- Introduced a new function `getFramerXML` to fetch and parse XML data.
- Modified `additionalPaths` to use data from `getFramerXML`.
- Adjusted the return structure of URLs in `additionalPaths`.

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

<!-- end pr-codex -->
2025-04-04 14:58:58 +00:00
MananTank 8f7bcaeadc Dashboard: Fix sitemap.xml and other files on public folder not loading (#6647) 2025-04-04 14:20:17 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ad7f78ca0b Version Packages (#6644)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-04 12:18:29 +02:00
Jonas DanielsandGitHub 0967a5d53a Fix newline at end of monad-testnet.ts (#6646) 2025-04-04 12:07:42 +02:00
KrishangandGitHub d93765ac54 [SDK] Feature: Export monadTestnet from thirdweb/chains (#6645) 2025-04-04 02:11:49 -07:00
169ae22a86 Add team capabilities to service-utils (#6641)
Co-authored-by: Phillip Ho <[email protected]>
2025-04-04 12:37:23 +08:00
YashandGitHub 04d2bc72e8 Update deploy config (#6643) 2025-04-04 16:55:41 +13:00
MananTank 1a236fd103 Dashboard: Storybook cleanup (#6642)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring Storybook stories across various components by standardizing the export format, updating layout parameters, and removing unnecessary imports. It aims to improve consistency and maintainability in the codebase.

### Detailed summary
- Changed `export const Desktop` to `export const Variants` in multiple stories.
- Removed `parameters: { layout: "centered" }` from many stories.
- Updated container classes for consistent styling.
- Removed `Toaster` components from stories.
- Refactored story decorators for better structure.
- Deleted unused imports like `mobileViewport` from several files.
- Consolidated `BadgeContainer` usage for better readability.

> The following files were skipped due to too many changes: `apps/dashboard/src/app/team/components/Analytics/CombinedBarChartCard.stories.tsx`, `apps/dashboard/src/app/team/[team_slug]/[project_slug]/nebula/components/analytics/nebula-analytics-ui.stories.tsx`, `apps/dashboard/src/app/nebula-app/(app)/components/ChatPageLayout.stories.tsx`, `apps/dashboard/src/app/(dashboard)/(chain)/[chain_id]/[contractAddress]/modules/components/batchMetadata.stories.tsx`, `apps/dashboard/src/components/dashboard/StepsCard.stories.tsx`, `apps/dashboard/src/@/components/blocks/SettingsCard.stories.tsx`

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

<!-- end pr-codex -->
2025-04-03 22:02:08 +00:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4691343c7d Version Packages (#6638)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 14:14:43 -07:00
Joaquim VergesandGitHub 93f225b8a3 [SDK] Fix ethers6 provider override (#6640) 2025-04-03 14:13:12 -07:00
Joaquim VergesandGitHub e9649a2b53 Update Next.js to 15.2.4 and other dependencies (#6632) 2025-04-03 12:51:19 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
4d39fb6ee3 Version Packages (#6620)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-03 12:28:27 -07:00
MananTank 1e092a7bff Dashboard: Fix timestamp ordering in various charts, migrate them to use ThirdwebBarChart (#6636)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily focuses on updating various components to improve chart functionalities and data handling in the dashboard, including the introduction of the `ThirdwebBarChart`. It also refines date handling and enhances the user interface for better data representation.

### Detailed summary
- Updated date handling in `stubbedData` from February to December.
- Introduced `BadgeContainer` for displaying time ranges in `TotalSponsoredChartCard`.
- Enhanced `searchParams` structure in the analytics page to include `from` and `to` dates.
- Added `customHeader`, `toolTipValueFormatter`, and `emptyChartState` to `ThirdwebBarChart`.
- Replaced `BarChart` with `ThirdwebBarChart` in multiple components for consistency.
- Improved data mapping and sorting in `TotalSponsoredChartCard`.
- Created new stubs for `EcosystemWalletUsersChartCard` to simulate user data over various days.
- Refactored empty state components to improve user guidance when no data is available.

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

<!-- end pr-codex -->
2025-04-03 18:36:21 +00:00
MananTank 80ee3d6670 [TOOL-3950] Dashboard: link to /explore page for contracts card on chain page (#6637)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR updates a link in the `products.ts` file for the dashboard component, changing the URL for a specific product.

### Detailed summary
- Updated the `link` property for the product from `https://thirdweb.com/contracts` to `https://thirdweb.com/explore`.

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

<!-- end pr-codex -->
2025-04-03 17:05:00 +00:00
MananTank a7fd4e51a3 [TOOL-3946] Dashboard: Fix in-app wallet users chart timestamp incorrect ordering (#6635) 2025-04-03 13:09:56 +00:00
MananTank 61583cf6f3 Dashboard: Fix project settings page max-width (#6634)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the layout of the `settings` page in the project dashboard. It enhances the styling and structure of the header and the content area.

### Detailed summary
- Replaced the existing `<h1>` element with a new structure wrapped in a `<div>` with a class of `border-b py-10`.
- Changed the font size of the `<h1>` from `text-3xl` to `text-2xl` for better responsiveness.
- Added a container with a maximum width of `7xl` around the header.
- Adjusted the padding of the content area to `pt-6 pb-20`.

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

<!-- end pr-codex -->
2025-04-03 12:58:26 +00:00
iuwqyir cf02af06a4 fix insight blueprints navigation (#6633)
## [Dashboard] Feature: Update API path format and add token lookup endpoint

## Notes for the reviewer

This PR updates the API path format in `insightBlueprints.ts` to use curly braces for path parameters (e.g., changing `/v1/transactions/:contractAddress` to `/v1/transactions/{contractAddress}`). This standardizes our path format to match OpenAPI specifications.

## How to test

The changes can be verified by checking the updated paths in the playground interface to ensure they display correctly with the new format.

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the URL path parameters in the `insightBlueprints` to use curly braces `{}` instead of colons `:` for better clarity and consistency in API endpoints.

### Detailed summary
- Changed path parameters from `:paramName` to `{paramName}` for various endpoints:
  - `/v1/transactions/:contractAddress` to `/v1/transactions/{contractAddress}`
  - `/v1/wallets/:wallet_address/transactions` to `/v1/wallets/{wallet_address}/transactions`
  - Updated multiple other endpoints similarly, including:
    - `/v1/events/:contractAddress` to `/v1/events/{contractAddress}`
    - `/v1/tokens/transfers/:contract_address` to `/v1/tokens/transfers/{contract_address}`
    - `/v1/nfts/:contract_address` to `/v1/nfts/{contract_address}`
    - And many more throughout the file.

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

<!-- end pr-codex -->
2025-04-03 12:15:39 +00:00
MananTank 6001f2487e [TOOL-3909] Dashboard: Add footer links to project pages & header style adjustments (#6627)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the layout and structure of various components in the application, introducing new footer sections, and improving the user interface for better organization and navigation.

### Detailed summary
- Removed the `SupportedPlatformLink.tsx` file.
- Updated `href` in `react-native/v5/page.mdx` from `/installation` to `/getting-started`.
- Enhanced layout in multiple components with added `container` classes for better styling.
- Introduced new `FooterCard` component in various pages for documentation and tutorial links.
- Replaced `AAFooterSection` with `AAFooter` in `AccountAbstractionPage.tsx`.
- Updated `InAppWalletFooterSection` to `InAppWalletsFooter` with a new structure.
- Modified headers and added spacing in several components for improved visual hierarchy.
- Introduced new `InsightFooter` component with relevant links.
- Adjusted various components to use consistent `container` class for layout consistency.

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

<!-- end pr-codex -->
2025-04-03 17:23:24 +05:30
Joaquim VergesandGitHub 3f21445369 [SDK] Emit accountChanged event when smart wallets switch chains (#6626) 2025-04-02 18:19:14 -07:00
MananTank 9e8fd89ec0 [TOOL-3942] Playground: Fix slow dev server - generate insight sidebar links with script (#6631)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new script for managing insight blueprints, updates the `getSidebarLinks` function to use pre-fetched blueprints, and modifies the `Page` component to display these blueprints. It also refines the utility functions for fetching blueprint data.

### Detailed summary
- Added `update-insight-blueprints` script in `package.json`.
- Created `insightBlueprints.ts` to store auto-generated blueprint data.
- Updated `getSidebarLinks` to use `insightBlueprints` instead of fetching blueprints dynamically.
- Modified `Page` component to render blueprints from `insightBlueprints`.
- Enhanced utility functions in `utils.ts` for fetching blueprint data.

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

<!-- end pr-codex -->
2025-04-02 23:28:15 +00:00
Joaquim VergesandGitHub 95a87530e3 [SDK] Fix updateListing date handling in marketplace direct listings (#6628) 2025-04-02 16:27:41 -07:00
0xFirekeeper 5faaf1be48 [Portal] Universal Bridge .NET (#6625)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on the removal of several payment-related pages in the `.NET SDK` documentation and the addition of a new section for the `Universal Bridge`, including examples and detailed API usage.

### Detailed summary
- Deleted pages:
  - `quickstart/page.mdx`
  - `buywithfiat/page.mdx`
  - `buywithcrypto/page.mdx`
  - `getbuyhistory/page.mdx`
  - `getbuywithfiatquote/page.mdx`
  - `getbuywithfiatstatus/page.mdx`
  - `getbuywithcryptoquote/page.mdx`
  - `getbuywithcryptostatus/page.mdx`
  - `getbuywithfiatcurrencies/page.mdx`
- Updated `sidebar.tsx` to remove payment links and add a new `Bridge` section.
- Introduced `Universal Bridge` documentation with:
  - Metadata creation
  - Core API examples for buying, selling, and transferring tokens.
  - Code snippets for `C#` usage of the `ThirdwebBridge`.

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

<!-- end pr-codex -->
2025-04-02 18:26:19 +00:00
jnsdls 4847ada819 Add /brand-kit to framer rewrites (#6624)
Added a rewrite rule for the `/brand-kit` path in the Framer rewrites configuration.
2025-04-02 17:53:14 +00:00
MananTank 08eea95f9b [TOOL-3764] Dashboard: Add Nebula FTUX (#6616)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR introduces a new `ClientIDSection` component to streamline the display of the Client ID across various components. It also modifies the `WaitingForIntegrationCard` to utilize this new component, improving code organization and reducing redundancy.

### Detailed summary
- Added `ClientIDSection` component to display the Client ID.
- Updated `InsightFTUX` to include `ClientIDSection` instead of inline code.
- Modified `WaitingForIntegrationCard` to accept children for rendering.
- Created `NebulaFTUX` component to integrate Nebula features and display Client ID.
- Adjusted styles in various components for consistency.

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

<!-- end pr-codex -->
2025-04-02 09:12:32 +00:00
MananTank 737155ba21 [TOOL-3765] Dashboard: Add Insight FTUX (#6614)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the `Insight` page by introducing new components for better integration and user experience, specifically around project insights and blueprints. It refactors existing code and adds new features for displaying API usage examples.

### Detailed summary
- Updated `isProjectActive` function to accept a structured parameter.
- Added `InsightFTUX` component for displaying integration instructions.
- Introduced `BlueprintCard` component with feature descriptions.
- Modified `WaitingForIntegrationCard` to display the client ID without a period.
- Enhanced `page.tsx` to conditionally render `InsightFTUX` or `BlueprintCard` based on project status.

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

<!-- end pr-codex -->
2025-04-02 09:12:31 +00:00
MananTank 73ae5817f2 [TOOL-3763] Dashboard: Update Contracts page (#6611)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the user interface and functionality of the contracts management section. It introduces new components and modifies existing ones to improve user experience, particularly around contract deployment and management.

### Detailed summary
- Removed the `GetStartedWithContractsDeploy` component.
- Added `DeployViaCLIOrImportCard` component for contract deployment options.
- Updated the `DeployedContractsPage` to include the new `DeployViaCLIOrImportCard`.
- Modified the `ContractTableUI` to display a message and button when no contracts are present.
- Increased the height of the no contracts message section.
- Added a link to discover contracts with an external icon.

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

<!-- end pr-codex -->
2025-04-02 09:12:31 +00:00
Joaquim VergesandGitHub 6a0704a19b Connect to current wallet chain when connecting other wallets in PayEmbed (#6621) 2025-04-02 02:09:39 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2ef8f2a371 chore(deps): bump biomejs/setup-biome from 2.3.0 to 2.5.0 (#6598)
Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-02 01:50:04 -07:00
d6373b2c67 [SDK] Feature: added ru_RU locale to thirdweb package (#6520)
Co-authored-by: Denis Vasilev <[email protected]>
Co-authored-by: Joaquim Verges <[email protected]>
2025-04-02 01:46:15 -07:00
MananTank 7eaf86a607 [TOOL-3762] Dashboard: Update In-app wallet analytics page (#6610)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring components related to in-app wallets and account abstraction analytics. It introduces new loading states, replaces certain components with `StatCard`, and enhances the handling of asynchronous data fetching.

### Detailed summary
- Removed `loading.tsx` files for in-app wallets and account abstraction.
- Updated `InAppWalletsHeader` to no longer accept props.
- Replaced `Stat` with `StatCard` in `EcosystemWalletsSummary` and `AccountAbstractionSummary`.
- Added `isPending` state to `StatCard` for loading indication.
- Refactored `InAppWalletsSummary` and `AccountAbstractionSummary` to utilize async data fetching with `Suspense`.
- Enhanced `InAppWalletAnalytics` to manage loading states and data fetching more effectively.

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

<!-- end pr-codex -->
2025-04-02 07:10:08 +00:00
gregGitHubgraphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
5788cbdbad [SDK] Feature: Adds action field to Bridge transactions (#6613)
Signed-off-by: greg <[email protected]>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
2025-04-02 00:08:03 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
d9a39cde33 Version Packages (#6619)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 19:43:56 -07:00
Joaquim VergesandGitHub 62c8e7c0c4 [SDK] show transfer amounts with fees (#6618) 2025-04-01 19:20:15 -07:00
Joaquim VergesandGitHub f618c5308c [SDK] Handle entrypoint resolution inside createAndSignUserOp (#6615) 2025-04-01 18:17:17 -07:00
gregandGitHub 04943d730b [Dashboard] Feature: Token symbol search (#6617) 2025-04-01 15:06:52 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2606da6d87 Version Packages (#6606)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-04-01 13:02:59 -07:00
kumaryash90 9cf137660a Remove custom config for Ronin (#6612)
Create2 deployer restrictions are lifted by the chain. Enable deployments through default create2 factory.

<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on removing the custom configuration for the `Ronin` blockchain within the `thirdweb` package.

### Detailed summary
- Removed the `Ronin` configuration from the gas price settings in the `create-2-factory.ts` file.
- Updated the gas price for `Ronin` from `110n * 10n ** 9n` to a static value of `110000n`.

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

<!-- end pr-codex -->
2025-04-01 19:06:06 +00:00
MananTank 58ab8c2ee7 [TOOL-3766] Dashboard: Account Abstraction page updates (#6609)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on improving the UI and functionality of various components in the dashboard by modifying styles, enhancing layout elements, and integrating new features related to account abstraction and user operation statistics.

### Detailed summary
- Removed `lg:gap-6` from grid classes in multiple components.
- Increased header sizes from `text-lg` to `text-xl` in `SponsoredTransactionsTableUI.tsx`.
- Adjusted margins and padding for improved spacing in various components.
- Added new `GasCreditAlert` component for displaying gas credit information.
- Integrated `AccountAbstractionSummary` into `AccountAbstractionAnalytics`.
- Updated `userOpStats` handling in `Page` component to use promises for better performance.
- Replaced certain text with more concise phrasing in descriptions.
- Removed unused imports and optimized component structure for clarity.

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

<!-- end pr-codex -->
2025-04-01 18:12:49 +00:00
MananTank 447a519ddc [TOOL-3768] Dashboard: Move Nebula from team to project layout (#6608)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on enhancing the `Nebula` components in the dashboard application by improving layout styles, adding project ID handling in analytics, and adjusting waitlist checks.

### Detailed summary
- Updated class names for better styling in `nebula-analytics-filter.tsx` and `nebula-waitlist-page.tsx`.
- Added `projectId` parameter to `fetchNebulaAnalytics`.
- Modified `NebulaWaitListPageUI` layout.
- Integrated `showNebula` prop in `ProjectSidebarLayout`.
- Adjusted project layout and waitlist checks in `layout.tsx`.
- Enhanced `NebulaAnalyticsPage` to pass `projectId`.
- Updated page handling in `page.tsx` to include project fetching and redirects.

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

<!-- end pr-codex -->
2025-04-01 11:56:32 +00:00
Joaquim VergesandGitHub a9b7633d05 [SDK] Show past transactions in transactions modal screen (#6607) 2025-04-01 03:14:28 -07:00
Joaquim VergesandGitHub 38e089a5f3 [SDK] Only onramp for required transaction amount (#6605) 2025-04-01 02:01:11 -07:00
Joaquim VergesandGitHub ef2f86cff0 Version Packages (#6589) 2025-03-31 23:05:33 -07:00
gregfromstl 8fdef82147 [Playground] Fix: Filter UB Backend API for only GETs (#6604)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the layout and functionality of the `Page` component in `apps/playground-web/src/app/connect/pay/backend/page.tsx`. It improves the presentation of the Universal Bridge REST API information and adds a link to view all endpoints.

### Detailed summary
- Wrapped the header and description in a `div` for better layout.
- Added a `Link` component with a button to "View all endpoints".
- Changed the title of the `BlueprintSection` from "Available endpoints" to "Available GET endpoints".
- Updated the filtering of `blueprints` to only include those with defined `get` properties.

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

<!-- end pr-codex -->
2025-04-01 01:02:26 +00:00
MananTank cbb15692a7 [TOOL-3880] Playground: Add utm_source on links (#6597)
<!-- start pr-codex -->

## PR-Codex overview
This PR primarily updates various `docsLink` URLs across multiple components in the `playground-web` application to include a query parameter (`?utm_source=playground`). This change likely aims to track traffic sources more effectively.

### Detailed summary
- Updated `docsLink` in `apps/playground-web/src/app/connect/auth/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/blockchain-api/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/commerce/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/fund-wallet/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/transactions/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/embed/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/button/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/sign-in/headless/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/in-app-wallet/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/insight/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/components/blocks/APIHeader.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/pay/backend/layout.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/connect/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/sponsor/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/nft/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/otherLinks.ts`
- Updated `docsLink` in `apps/playground-web/src/app/connect/account-abstraction/native-aa/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/chain/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/token/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/social/page.tsx`
- Updated `docsLink` in `apps/playground-web/src/app/connect/ui/wallet/page.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/pay/embed/LeftSection.tsx`
- Updated `href` in `apps/playground-web/src/app/insight/page.tsx`
- Updated `href` in `apps/playground-web/src/components/auth/gated-content.tsx`
- Updated `href` in `apps/playground-web/src/components/engine/minting/erc1155-mint-to.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/sign-in/components/InAppWalletFormGroup.tsx`
- Updated `href` in `apps/playground-web/src/app/connect/sign-in/button/LeftSection.tsx`

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

<!-- end pr-codex -->
2025-03-31 19:37:09 +00:00
MananTank 1d23e5cd9c Dashboard: Fix AA page alert styles (#6596)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on refactoring the `SmartWalletsBillingAlert` component in the `Alerts.tsx` file. It updates the alert's UI and removes the dismissible functionality, while also changing the import paths for components used in the alert.

### Detailed summary
- Removed unused imports related to `Alert`, `AlertDescription`, `AlertIcon`, `AlertTitle`, `Flex`, and `IconButton`.
- Updated imports for `Alert`, `AlertDescription`, `AlertTitle`, `TrackedUnderlineLink`, and `CircleAlertIcon`.
- Removed the `dismissible` prop and related logic.
- Simplified the alert structure with a new layout.
- Changed the alert icon and styling.

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

<!-- end pr-codex -->
2025-03-31 19:07:57 +00:00
MananTank a17d893811 [TOOL-3873] Fix pageview tracking in dashboard, Add posthog to playground (#6595) 2025-03-31 18:38:58 +00:00
kumaryash90 0dcde077f5 [SDK] Fix bytecode caching (#6593)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on enhancing the bytecode caching mechanism in the `thirdweb` package by including additional metadata in the cache key, which improves cache differentiation and management.

### Detailed summary
- Updated the `cacheKey` in `deploy-metadata.ts` to include `compilerMetadata.publisher` and `compilerMetadata.version` along with `compilerMetadata.name` and `chain.id`.
- Removed the `version` and `publisher` fields from the deployment test case in `process-ref-deployments.test.ts`.

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

<!-- end pr-codex -->
2025-03-31 18:30:28 +00:00
jnsdls 05a041fac3 Add glossary slug route to framer rewrites (#6594)
### TL;DR

Added a rewrite rule for glossary detail pages in the dashboard app.

### What changed?

Added a new path `/learn/glossary/:glossary_slug` to the framer-rewrites.js file to support individual glossary term pages.

### How to test?

1. Navigate to a glossary term detail page (e.g., `/learn/glossary/some-term`)
2. Verify that the page loads correctly and the routing works as expected

### Why make this change?

To enable proper routing for individual glossary term pages, allowing users to access specific glossary entries directly through their unique URLs. This complements the existing glossary index page that was already configured.
2025-03-31 18:00:30 +00:00
MananTank 3d140eed97 [TOOL-3874] Portal: Fix pl.js script CSP error (#6591)
<!--

## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes"

If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000):

## Notes for the reviewer

Anything important to call out? Be sure to also clarify these in your comments.

## How to test

Unit tests, playground, etc.

-->

<!-- start pr-codex -->

---

## PR-Codex overview
This PR focuses on updating the Content Security Policy in the `next.config.mjs` file to enhance security by modifying the `script-src` directive.

### Detailed summary
- Updated the `script-src` directive to include `thirdweb.com` and `thirdweb-dev.com` without a wildcard prefix.
- Retained existing sources for `self`, `unsafe-eval`, `unsafe-inline`, `wasm-unsafe-eval`, `inline-speculation-rules`, `vercel.live`, and `js.stripe.com`.

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

<!-- end pr-codex -->
2025-03-31 17:43:57 +00:00
jnsdls a5f005fff0 Remove tutorial routes from framer rewrites (#6592)
closes: CORE-802

Redirect `/learn/tutorials` to `/learn/guides`

This PR removes `/learn/tutorials` and `/learn/tutorials/:tutorial_slug` from the framer-rewrites list and adds redirects from these paths to their corresponding `/learn/guides` equivalents.
2025-03-31 17:14:04 +00:00
saminacodes fb70cab6d4 [portal] added module warning (#6590)
CORE-0000

<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a warning callout to the `page.mdx` file in the `cross-chain/agglayer` directory, informing users that the Agglayer module is in beta and has deployment limitations.

### Detailed summary
- Added a warning callout indicating that the Agglayer module is in beta.
- Specified that deployments are limited to `Sepolia` and `Cardona` due to requirements for Ethereum as L1 and zkEVM as L2.

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

<!-- end pr-codex -->
2025-03-31 14:53:39 +00:00
Joaquim VergesandGitHub cd9ea69c13 [SDK] Add erc20Value for makeOffer extension function (#6588) 2025-03-31 00:24:42 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
dc892bd136 Version Packages (#6585)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-30 16:14:40 -07:00
Joaquim VergesandGitHub 3d7c009d72 [CLI] Fix secret key handling (#6584) 2025-03-30 15:49:45 -07:00
YashandGitHub 6dbed0d4fc [SDK] fix: Use default contract params with optional overrides (#6581) 2025-03-30 15:45:13 -07:00
MananTank adee1903d5 [TOOL-3814] Dashboard: Show all active coupons in team billing page (#6568)
<!-- start pr-codex -->

## PR-Codex overview
This PR focuses on refactoring the coupon management system within the billing section of the dashboard. It introduces a new structure for handling coupons, including a client component for API interactions and UI components for displaying and applying coupons.

### Detailed summary
- Deleted obsolete files: `CouponCard.tsx`, `CouponDetails.stories.tsx`, `ApplyCouponCard.stories.tsx`.
- Updated global styles import in `preview.tsx`.
- Replaced `CouponSection` with `Coupons` in `index.tsx`.
- Introduced `CouponsClient` for managing coupon API interactions.
- Created `AsyncCoupons` to fetch coupon data asynchronously.
- Developed `Coupons` component to handle loading states and display.
- Added `CouponsUI` for rendering coupon information and actions.
- Implemented coupon application and deletion logic with error handling.
- Created a story for `CouponsUI` to demonstrate different states and interactions.

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

<!-- end pr-codex -->
2025-03-30 22:18:56 +00:00
gregandGitHub a1d785b8ed Fix: Increase commerce price to avoid minimum purchase errors (#6582) 2025-03-29 20:50:25 -07:00
Joaquim VergesGitHubgithub-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ba92cd2cbe Version Packages (#6575)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-03-29 03:41:26 -07:00
Joaquim VergesandGitHub c3d0ea0059 [PayEmbed] Respect supportedTokens prop (#6580) 2025-03-29 03:35:22 -07:00
MananTank 35c5f57179 [TOOL-3839] Fix double scrollbars in project settings page (#6579) 2025-03-29 07:51:23 +00:00
gregandGitHub 2a1f4be48b Fix: Not Found Status should show Pending (#6578) 2025-03-28 20:11:43 -07:00
gregandGitHub 0121ccd702 fix: reduce cost of playground commerce example (#6577) 2025-03-28 19:19:03 -07:00
Toomas OosaluandGitHub 12bcfc596a insight webhooks docs (#6574) 2025-03-29 11:11:25 +13:00
MananTank a4ff738268 [TOOL-3833] Dashboard: Fix overflow scroll created by sr-only element (#6576) 2025-03-28 19:17:40 +00:00
YashandGitHub b49c191355 Support stylus contracts publish and deploy (#6495) 2025-03-29 07:34:37 +13:00
saminaandGitHub 06f0765665 Update page.mdx (#6572)
Signed-off-by: samina <[email protected]>
2025-03-27 19:54:50 -07:00
449 changed files with 15636 additions and 12021 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
uses: ./.github/composite-actions/install
- name: Setup Biome
uses: biomejs/setup-biome@c016c38f26f2c4a6eb3662679143614a254263fd # v2.3.0
uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
with:
version: latest
+2
View File
@@ -0,0 +1,2 @@
public-hoist-pattern[]=*import-in-the-middle*
public-hoist-pattern[]=*require-in-the-middle*
+9 -14
View File
@@ -1,5 +1,5 @@
import type { Preview } from "@storybook/react";
import "@/styles/globals.css";
import "../src/global.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MoonIcon, SunIcon } from "lucide-react";
import { ThemeProvider, useTheme } from "next-themes";
@@ -81,23 +81,18 @@ function StoryLayout(props: {
return (
<QueryClientProvider client={queryClient}>
<div className="flex min-h-dvh min-w-0 flex-col bg-background text-foreground">
<div className="flex justify-end gap-2 border-b p-4">
<div className="fixed right-0 bottom-0 z-50 flex justify-end gap-2 p-4">
<Button
onClick={() => setTheme("dark")}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
size="sm"
variant={theme === "dark" ? "default" : "outline"}
variant="outline"
className="h-auto w-auto shrink-0 rounded-full p-2"
>
<MoonIcon className="size-4" />
</Button>
<Button
onClick={() => setTheme("light")}
size="sm"
variant={theme === "light" ? "default" : "outline"}
className="h-auto w-auto shrink-0 rounded-full p-2"
>
<SunIcon className="size-4" />
{theme === "dark" ? (
<MoonIcon className="size-4" />
) : (
<SunIcon className="size-4" />
)}
</Button>
</div>
+3 -2
View File
@@ -49,12 +49,13 @@ module.exports = [
"/templates/:template_slug",
// -- learn --
"/learn",
"/learn/tutorials",
"/learn/tutorials/:tutorial_slug",
"/learn/guides",
"/learn/guides/:guide_slug",
"/learn/courses",
"/learn/glossary",
"/learn/glossary/:glossary_slug",
// -- faucets --
"/faucets",
// -- brand kit --
"/brand-kit",
];
+2 -1
View File
@@ -12,6 +12,7 @@
"ignoreDependencies": [
"@storybook/blocks",
"@thirdweb-dev/service-utils",
"@types/color"
"@types/color",
"fast-xml-parser"
]
}
+32 -10
View File
@@ -1,7 +1,9 @@
// @ts-check
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { XMLParser } = require("fast-xml-parser");
/**
*
* @returns {Promise<Array<{chainId: number, name: string, slug: string}>>}
*/
async function fetchChainsFromApi() {
const res = await fetch("https://api.thirdweb.com/v1/chains", {
@@ -82,16 +84,20 @@ module.exports = {
};
},
additionalPaths: async (config) => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const FRAMER_PATHS = require("./framer-rewrites");
const allChains = await fetchChainsFromApi();
const [framerUrls, allChains] = await Promise.all([
getFramerXML(),
fetchChainsFromApi(),
]);
return [
...FRAMER_PATHS.map((path) => ({
loc: path,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
})),
...framerUrls.map((url) => {
return {
loc: url.loc,
changefreq: config.changefreq,
priority: config.priority,
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
};
}),
...allChains.map((chain) => {
return {
loc: `/${chain.slug}`,
@@ -136,3 +142,19 @@ async function createSearchRecordSitemaps(config) {
// filter out any failed requests
return chainsForLines.filter(Boolean);
}
async function getFramerXML() {
const framerSiteMapText = await fetch(
"https://landing.thirdweb.com/sitemap.xml",
).then((res) => res.text());
const parser = new XMLParser();
const xmlObject = parser.parse(framerSiteMapText);
/**
* @type {Array<{loc: string}>}
*/
const urls = xmlObject.urlset.url;
return urls;
}
-1
View File
@@ -203,7 +203,6 @@ function getConfig(): NextConfig {
type: "memory",
});
}
config.externals.push("pino-pretty");
config.module = {
...config.module,
exprContextCritical: false,
+50 -48
View File
@@ -29,29 +29,29 @@
"@emotion/styled": "11.14.0",
"@hookform/resolvers": "^3.9.1",
"@marsidev/react-turnstile": "^1.1.0",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.6",
"@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.6",
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3",
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-separator": "^1.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tooltip": "1.1.8",
"@sentry/nextjs": "9.5.0",
"@shazow/whatsabi": "0.20.0",
"@tanstack/react-query": "5.67.3",
"@radix-ui/react-accordion": "^1.2.4",
"@radix-ui/react-alert-dialog": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.4",
"@radix-ui/react-checkbox": "^1.1.5",
"@radix-ui/react-dialog": "1.1.7",
"@radix-ui/react-dropdown-menu": "^2.1.7",
"@radix-ui/react-hover-card": "^1.1.7",
"@radix-ui/react-label": "^2.1.3",
"@radix-ui/react-popover": "^1.1.7",
"@radix-ui/react-progress": "^1.1.3",
"@radix-ui/react-radio-group": "^1.2.4",
"@radix-ui/react-select": "^2.1.7",
"@radix-ui/react-separator": "^1.1.3",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.1.4",
"@radix-ui/react-tooltip": "1.2.0",
"@sentry/nextjs": "9.12.0",
"@shazow/whatsabi": "0.21.0",
"@tanstack/react-query": "5.72.1",
"@tanstack/react-table": "^8.21.2",
"@thirdweb-dev/service-utils": "workspace:*",
"@vercel/functions": "2.0.0",
"@vercel/og": "^0.6.5",
"@vercel/og": "^0.6.8",
"abitype": "1.0.8",
"chakra-react-select": "^4.7.6",
"class-variance-authority": "^0.7.1",
@@ -59,6 +59,7 @@
"color": "5.0.0",
"compare-versions": "^6.1.0",
"date-fns": "4.1.0",
"fast-xml-parser": "^5.2.0",
"fetch-event-stream": "0.1.5",
"flat": "^6.0.1",
"framer-motion": "12.5.0",
@@ -67,8 +68,8 @@
"input-otp": "^1.4.1",
"ioredis": "^5.6.0",
"ipaddr.js": "^2.2.0",
"lucide-react": "0.483.0",
"next": "15.2.3",
"lucide-react": "0.487.0",
"next": "15.2.4",
"next-plausible": "^3.12.4",
"next-themes": "^0.4.6",
"nextjs-toploader": "^1.6.12",
@@ -77,25 +78,26 @@
"papaparse": "^5.5.2",
"pluralize": "^8.0.0",
"posthog-js": "1.67.1",
"prettier": "3.5.3",
"qrcode": "^1.5.3",
"react": "19.0.0",
"react": "19.1.0",
"react-children-utilities": "^2.10.0",
"react-day-picker": "^8.10.1",
"react-dom": "19.0.0",
"react-dom": "19.1.0",
"react-dropzone": "^14.3.8",
"react-error-boundary": "^5.0.0",
"react-hook-form": "7.54.2",
"react-hook-form": "7.55.0",
"react-markdown": "^9.0.1",
"react-table": "^7.8.0",
"recharts": "2.15.1",
"recharts": "2.15.2",
"remark-gfm": "4.0.1",
"responsive-rsc": "0.0.7",
"server-only": "^0.0.1",
"shiki": "1.27.0",
"sonner": "2.0.1",
"sonner": "2.0.3",
"spdx-correct": "^3.2.0",
"stripe": "17.7.0",
"swagger-ui-react": "^5.20.1",
"swagger-ui-react": "^5.20.7",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7",
"thirdweb": "workspace:*",
@@ -106,41 +108,41 @@
"devDependencies": {
"@chakra-ui/cli": "^2.4.1",
"@chromatic-com/storybook": "3.2.6",
"@next/bundle-analyzer": "15.2.3",
"@next/eslint-plugin-next": "15.2.3",
"@next/bundle-analyzer": "15.2.4",
"@next/eslint-plugin-next": "15.2.4",
"@playwright/test": "1.51.1",
"@storybook/addon-essentials": "8.6.4",
"@storybook/addon-interactions": "8.6.4",
"@storybook/addon-links": "8.6.4",
"@storybook/addon-onboarding": "8.6.4",
"@storybook/addon-viewport": "8.6.4",
"@storybook/blocks": "8.6.4",
"@storybook/nextjs": "8.6.4",
"@storybook/react": "8.6.4",
"@storybook/test": "8.6.4",
"@storybook/addon-essentials": "8.6.12",
"@storybook/addon-interactions": "8.6.12",
"@storybook/addon-links": "8.6.12",
"@storybook/addon-onboarding": "8.6.12",
"@storybook/addon-viewport": "8.6.12",
"@storybook/blocks": "8.6.12",
"@storybook/nextjs": "8.6.12",
"@storybook/react": "8.6.12",
"@storybook/test": "8.6.12",
"@types/color": "4.2.0",
"@types/node": "22.13.10",
"@types/node": "22.14.0",
"@types/papaparse": "^5.3.15",
"@types/pluralize": "^0.0.33",
"@types/qrcode": "^1.5.5",
"@types/react": "19.0.10",
"@types/react-dom": "19.0.4",
"@types/react": "19.1.0",
"@types/react-dom": "19.1.1",
"@types/react-table": "^7.7.20",
"@types/spdx-correct": "^3.1.3",
"@types/swagger-ui-react": "^4.19.0",
"@typescript-eslint/eslint-plugin": "7.14.1",
"@typescript-eslint/parser": "7.14.1",
"autoprefixer": "^10.4.21",
"checkly": "5.0.1",
"checkly": "5.2.0",
"eslint": "8.57.0",
"eslint-config-biome": "1.9.4",
"eslint-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301",
"eslint-plugin-storybook": "0.11.6",
"knip": "5.46.0",
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
"eslint-plugin-storybook": "0.12.0",
"knip": "5.47.0",
"next-sitemap": "^4.2.3",
"postcss": "8.5.3",
"storybook": "8.6.4",
"storybook": "8.6.12",
"tailwindcss": "3.4.17",
"typescript": "5.8.2"
"typescript": "5.8.3"
}
}
+13
View File
@@ -361,6 +361,19 @@ async function redirects() {
"/team/:team_slug/:project_slug/connect/universal-bridge/:path*",
permanent: false,
},
// all /learn/tutorials (and sub-routes) -> /learn/guides
{
source: "/learn/tutorials/:path*",
destination: "/learn/guides/:path*",
permanent: false,
},
{
source: "/learn/tutorials",
destination: "/learn/guides",
permanent: false,
},
...legacyDashboardToTeamRedirects,
];
}
+4 -3
View File
@@ -270,9 +270,10 @@ type ActiveStatus = {
ecosystemWallet: boolean;
};
export async function isProjectActive(
params: AnalyticsQueryParams,
): Promise<ActiveStatus> {
export async function isProjectActive(params: {
teamId: string;
projectId: string;
}): Promise<ActiveStatus> {
const searchParams = buildSearchParams(params);
const res = await fetchAnalytics(
`v2/active-usage?${searchParams.toString()}`,
@@ -8,13 +8,12 @@ import { GradientAvatar } from "./GradientAvatar";
const meta = {
title: "blocks/Avatars/GradientAvatar",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -22,14 +21,14 @@ const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<p> All images below are set with size-6 className </p>
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<p> All images below are set with size-20 className </p>
<BadgeContainer label="No Src, No id - Skeleton">
<GradientAvatar
id={undefined}
src={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -38,7 +37,7 @@ function Story() {
<GradientAvatar
id={"foo"}
src={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -47,7 +46,7 @@ function Story() {
<GradientAvatar
id={"foo"}
src={""}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -56,7 +55,7 @@ function Story() {
<GradientAvatar
id={"bar"}
src={""}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -65,7 +64,7 @@ function Story() {
<GradientAvatar
src="invalid-src"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -74,7 +73,7 @@ function Story() {
<GradientAvatar
src="https://picsum.photos/200/300"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -83,7 +82,7 @@ function Story() {
<GradientAvatar
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
id={undefined}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
@@ -123,14 +122,14 @@ function ToggleTest() {
<GradientAvatar
src={data?.src}
id={data?.id}
className="size-6"
className="size-20"
client={client}
/>
</BadgeContainer>
<BadgeContainer label="invalid Src">
<GradientAvatar
className="size-6"
className="size-20"
src={data ? "invalid-src" : undefined}
id={undefined}
client={client}
@@ -14,7 +14,7 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -22,7 +22,7 @@ const client = getThirdwebClient();
function Story() {
return (
<div className="flex flex-col gap-10 p-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<p> All images below are set with size-6 className </p>
<BadgeContainer label="No Src - Skeleton">
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { DangerSettingCard } from "./DangerSettingCard";
const meta = {
@@ -15,17 +15,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
@@ -9,13 +9,12 @@ import { Img } from "./Img";
const meta = {
title: "blocks/Img",
component: Story,
parameters: {},
} satisfies Meta<typeof Story>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { MultiNetworkSelector } from "./NetworkSelectors";
const meta = {
@@ -16,20 +16,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-8 py-10">
<Variant label="No Chains selected by default" selectedChainIds={[]} />
<Variant
label="Polygon, Ethereum selected by default"
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SettingsCard } from "./SettingsCard";
const meta = {
@@ -15,139 +15,130 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="min-h-dvh bg-background py-6 text-foreground">
<div className="container flex max-w-[1100px] flex-col gap-10 lg:p-10">
<BadgeContainer label="No Header">
<SettingsCard
bottomText="This is bottom text"
header={undefined}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="No Header">
<SettingsCard
bottomText="This is bottom text"
header={undefined}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Header">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Header">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText={undefined}
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Error">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText="This is error text"
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Error">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
errorText="This is error text"
noPermissionText={undefined}
saveButton={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="No Permission">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText="You do not have permission to edit this"
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="No Permission">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText="You do not have permission to edit this"
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Disabled">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: true,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Disabled">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: true,
isPending: false,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
<BadgeContainer label="Save Loading">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
</div>
<BadgeContainer label="Save Loading">
<SettingsCard
bottomText="This is bottom text"
header={{
title: "Some Title",
description: "This is some description for this card",
}}
saveButton={{
disabled: false,
isPending: true,
onClick: () => {},
}}
noPermissionText={undefined}
errorText={undefined}
>
<ChildrenPlaceholder />
</SettingsCard>
</BadgeContainer>
</div>
);
}
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react";
import { mobileViewport } from "../../../stories/utils";
import { SidebarLayout } from "./SidebarLayout";
const meta = {
@@ -15,17 +14,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<SidebarLayout
@@ -17,6 +17,7 @@ import { NavLink } from "../ui/NavLink";
import { Separator } from "../ui/separator";
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
import { CustomSidebar, type SidebarLink } from "./Sidebar";
import { AppFooter } from "./app-footer";
export function SidebarLayout(props: {
sidebarLinks: SidebarLink[];
@@ -54,15 +55,17 @@ export function FullWidthSidebarLayout(props: {
footerSidebarLinks?: SidebarLink[];
children: React.ReactNode;
className?: string;
footer?: React.ReactNode;
}) {
const { contentSidebarLinks, children, footerSidebarLinks } = props;
return (
<div
className={cn("flex w-full flex-1 overflow-y-hidden", props.className)}
className={cn(
"relative flex w-full flex-1 overflow-y-hidden",
props.className,
)}
>
{/* left - sidebar */}
<Sidebar collapsible="icon" side="left">
<Sidebar collapsible="icon" side="left" className="pt-2">
<SidebarContent>
<SidebarGroup>
<SidebarGroupContent>
@@ -92,10 +95,10 @@ export function FullWidthSidebarLayout(props: {
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
/>
<main className="container z-0 flex min-w-0 max-w-[1280px] grow flex-col pb-20 max-sm:w-full lg:pt-6">
<main className="z-0 flex min-w-0 grow flex-col max-sm:w-full">
{children}
</main>
{props.footer}
<AppFooter containerClassName="max-w-7xl" />
</div>
</div>
);
@@ -153,7 +156,7 @@ function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
const activeLink = useActiveSidebarLink(props.links);
return (
<div className="mb-4 flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
<SidebarTrigger className="size-4" />
<Separator orientation="vertical" className="h-4" />
{activeLink && <span className="text-sm">{activeLink.label}</span>}
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SingleNetworkSelector } from "./NetworkSelectors";
const meta = {
@@ -16,20 +16,13 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
<div className="container flex max-w-6xl flex-col gap-8 py-10">
<Variant label="No Chain ID selected by default" chainId={undefined} />
<Variant label="Polygon selected by default" chainId={137} />
<Variant
@@ -12,17 +12,23 @@ import Link from "next/link";
type AppFooterProps = {
className?: string;
containerClassName?: string;
};
export function AppFooter(props: AppFooterProps) {
return (
<footer
className={cn(
"w-full border-border border-t bg-card py-6 md:py-8",
"w-full border-border border-t py-6 md:py-8",
props.className,
)}
>
<div className="container flex flex-col gap-4 md:gap-6">
<div
className={cn(
"container flex flex-col gap-4 md:gap-6",
props.containerClassName,
)}
>
{/* top row */}
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div className="flex flex-row items-center gap-3">
@@ -30,6 +30,7 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
};
customHeader?: React.ReactNode;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -60,6 +61,8 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
</CardHeader>
)}
{props.customHeader && props.customHeader}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
@@ -32,6 +32,7 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
description?: string;
titleClassName?: string;
};
customHeader?: React.ReactNode;
// chart config
config: TConfig;
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
@@ -41,7 +42,9 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
chartClassName?: string;
isPending: boolean;
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
hideLabel?: boolean;
emptyChartState?: React.ReactElement;
};
export function ThirdwebBarChart<TConfig extends ChartConfig>(
@@ -65,12 +68,14 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
</CardHeader>
)}
{props.customHeader && props.customHeader}
<CardContent className={cn(!props.header && "pt-6")}>
<ChartContainer config={props.config} className={props.chartClassName}>
{props.isPending ? (
<LoadingChartState />
) : props.data.length === 0 ? (
<EmptyChartState />
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
) : (
<BarChart accessibilityLayer data={props.data}>
<CartesianGrid vertical={false} />
@@ -88,6 +93,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
props.hideLabel !== undefined ? props.hideLabel : true
}
labelFormatter={props.toolTipLabelFormatter}
valueFormatter={props.toolTipValueFormatter}
/>
}
/>
@@ -96,25 +102,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
content={<ChartLegendContent className="pt-5" />}
/>
)}
{configKeys.map((key, idx) => (
{configKeys.map((key) => (
<Bar
key={key}
dataKey={key}
// if stacked then they should all be the same stackId
// if grouped then they should all be unique stackId (so the key works great)
stackId={variant === "stacked" ? "a" : key}
fill={`var(--color-${key})`}
// if stacked then we need to figure out the radius based on the index in the array
// if grouped then we can just use the same radius for all
radius={
variant === "stacked"
? idx === 0
? [0, 0, 4, 4]
: idx === configKeys.length - 1
? [4, 4, 0, 0]
: [0, 0, 0, 0]
: [4, 4, 4, 4]
}
fill={props.config[key]?.color}
radius={[4, 4, 4, 4]}
strokeWidth={1}
className="stroke-background"
/>
@@ -8,7 +8,7 @@ import {
} from "@/components/ui/select";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { type CodeEnvironment, CodeSegment } from "./code-segment.client";
const meta = {
@@ -24,17 +24,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
type Mode = "default" | "no-tabs" | "only-tabs";
function Story() {
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { MultiSelect } from "./multi-select";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
@@ -36,7 +29,7 @@ function createList(len: number) {
function Story() {
return (
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
<div className="container flex max-w-6xl flex-col gap-6 py-10">
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
<VariantTest
@@ -3,33 +3,35 @@ import type { Team } from "@/api/team";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { cn } from "@/lib/utils";
import { CheckIcon, CircleAlertIcon, CircleDollarSignIcon } from "lucide-react";
import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
import Link from "next/link";
import type React from "react";
import { TEAM_PLANS } from "utils/pricing";
import { useTrack } from "../../../hooks/analytics/useTrack";
import { remainingDays } from "../../../utils/date-utils";
import type { GetBillingCheckoutUrlAction } from "../../actions/billing";
import type { ProductSKU } from "../../lib/billing";
import { CheckoutButton } from "../billing";
type ButtonProps = React.ComponentProps<typeof Button>;
const PRO_CONTACT_US_URL =
"https://meetings.hubspot.com/sales-thirdweb/thirdweb-pro";
type PricingCardCta = {
hint?: string;
title: string;
onClick?: () => void;
} & (
| {
type: "link";
href: string;
}
| {
type: "checkout";
}
);
type PricingCardProps = {
teamSlug: string;
billingPlan: Exclude<Team["billingPlan"], "free">;
cta?: {
hint?: string;
title: string;
tracking: {
category: string;
label?: string;
};
variant?: ButtonProps["variant"];
onClick?: () => void;
};
billingPlan: keyof typeof TEAM_PLANS;
cta?: PricingCardCta;
ctaHint?: string;
highlighted?: boolean;
current?: boolean;
@@ -49,13 +51,23 @@ export const PricingCard: React.FC<PricingCardProps> = ({
const plan = TEAM_PLANS[billingPlan];
const isCustomPrice = typeof plan.price === "string";
const trackEvent = useTrack();
const remainingTrialDays =
(activeTrialEndsAt ? remainingDays(activeTrialEndsAt) : 0) || 0;
const handleCTAClick = () => {
cta?.onClick?.();
trackEvent({
category: "account",
label: `${billingPlan}Plan`,
action: "click",
});
};
return (
<div
className={cn(
"z-10 flex w-full flex-col gap-6 rounded-xl border border-border bg-card p-4 md:p-6",
"z-10 flex w-full flex-col gap-4 rounded-xl border border-border bg-card p-4",
current && "border-blue-500",
highlighted && "border-active-border",
)}
@@ -71,13 +83,13 @@ export const PricingCard: React.FC<PricingCardProps> = ({
<div className="flex flex-col gap-5">
{/* Title + Desc */}
<div>
<div className="mb-2 flex flex-row items-center gap-2">
<div className="mb-1 flex flex-row items-center gap-3">
<h3 className="font-semibold text-2xl capitalize tracking-tight">
{plan.title}
</h3>
{current && <Badge className="capitalize">Current plan</Badge>}
</div>
<p className="max-w-[320px] text-muted-foreground">
<p className="max-w-[320px] text-muted-foreground text-sm">
{plan.description}
</p>
</div>
@@ -85,30 +97,13 @@ export const PricingCard: React.FC<PricingCardProps> = ({
{/* Price */}
<div className="flex flex-col gap-0.5">
<div className="flex items-center gap-2">
<span className="font-semibold text-3xl text-foreground tracking-tight">
<span className="font-semibold text-2xl text-foreground tracking-tight">
${plan.price}
</span>
{!isCustomPrice && (
<span className="text-muted-foreground">/ month</span>
)}
{billingPlan === "starter" && (
<ToolTipLabel
contentClassName="max-w-[320px]"
label="We will place a temporary hold of $25 to verify your card, this will be immediately released back to you after verification."
>
<Button
asChild
variant="ghost"
className="h-auto w-auto p-1 text-muted-foreground hover:text-foreground"
>
<div>
<CircleAlertIcon className="size-5 shrink-0" />
</div>
</Button>
</ToolTipLabel>
)}
</div>
{remainingTrialDays > 0 && (
@@ -124,7 +119,7 @@ export const PricingCard: React.FC<PricingCardProps> = ({
<div className="flex grow flex-col items-start gap-2 text-foreground">
{plan.subTitle && (
<p className="font-medium text-foreground">{plan.subTitle}</p>
<p className="font-medium text-foreground text-sm">{plan.subTitle}</p>
)}
{plan.features.map((f) => (
@@ -134,29 +129,30 @@ export const PricingCard: React.FC<PricingCardProps> = ({
{cta && (
<div className="flex flex-col gap-3">
{billingPlan !== "pro" ? (
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
<CheckoutButton
buttonProps={{
variant: cta.variant || "outline",
className: "gap-2",
onClick: cta.onClick,
variant: highlighted ? "default" : "outline",
className: highlighted ? undefined : "bg-background",
onClick: handleCTAClick,
}}
teamSlug={teamSlug}
sku={billingPlan === "starter" ? "plan:starter" : "plan:growth"}
sku={billingPlanToSkuMap[billingPlan]}
getBillingCheckoutUrl={getBillingCheckoutUrl}
>
{cta.title}
</CheckoutButton>
) : (
<Button variant={cta.variant || "outline"} asChild>
<TrackedLinkTW
href={PRO_CONTACT_US_URL}
label={cta.tracking?.label}
category={cta.tracking?.category}
target="_blank"
>
)}
{cta.type === "link" && (
<Button
variant={highlighted ? "default" : "outline"}
className={highlighted ? undefined : "bg-background"}
asChild
>
<Link href={cta.href} target="_blank" onClick={handleCTAClick}>
{cta.title}
</TrackedLinkTW>
</Link>
</Button>
)}
@@ -171,6 +167,19 @@ export const PricingCard: React.FC<PricingCardProps> = ({
);
};
const billingPlanToSkuMap: Record<Team["billingPlan"], ProductSKU | undefined> =
{
starter: "plan:starter",
growth: "plan:growth",
accelerate: "plan:accelerate",
scale: "plan:scale",
// we can't render checkout buttons for these plans:
pro: undefined,
free: undefined,
growth_legacy: undefined,
starter_legacy: undefined,
};
type FeatureItemProps = {
text: string | string[];
};
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useMemo, useState } from "react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { SelectWithSearch } from "./select-with-search";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function createList(len: number) {
return Array.from({ length: len }, (_, i) => ({
value: `${i}`,
@@ -36,7 +29,7 @@ function createList(len: number) {
function Story() {
return (
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
<div className="container flex max-w-6xl flex-col gap-6 py-10">
<VariantTest storyLabel="5 items" listLen={5} />
<VariantTest storyLabel="5000 items" listLen={5000} />
<VariantTest
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { BadgeContainer, mobileViewport } from "../../stories/utils";
import { BadgeContainer } from "../../stories/utils";
import { PaginationButtons } from "./pagination-buttons";
const meta = {
@@ -16,17 +16,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Story() {
return (
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
@@ -96,7 +96,10 @@ const BreadcrumbEllipsis = ({
<span
role="presentation"
aria-hidden="true"
className={cn("flex h-9 w-9 items-center justify-center", className)}
className={cn(
"relative flex h-9 w-9 items-center justify-center",
className,
)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
@@ -6,9 +6,6 @@ import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/Buttons",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { CodeClient } from "./code.client";
const meta = {
@@ -11,17 +11,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const tsCode = `\
type User = {
name: string;
@@ -86,7 +79,7 @@ const bashCode = "pnpm i thirdweb";
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="ts">
<CodeClient code={tsCode} lang="ts" />
</BadgeContainer>
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { PlainTextCodeBlock } from "./plaintext-code";
const meta = {
@@ -11,17 +11,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const jsCode = `\
import { getContract } from "thirdweb";
import { sepolia } from "thirdweb/chains";
@@ -39,7 +32,7 @@ const overflowText =
function Component() {
return (
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="large chunk of code">
<PlainTextCodeBlock code={jsCode} />
</BadgeContainer>
@@ -94,7 +94,10 @@ const PaginationEllipsis = ({
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
className={cn(
"relative flex h-9 w-9 items-center justify-center",
className,
)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
@@ -13,9 +13,6 @@ import { BadgeContainer } from "../../../stories/utils";
const meta = {
title: "Shadcn/Select",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
@@ -10,35 +10,25 @@ import {
} from "@/components/ui/table";
import type { Meta, StoryObj } from "@storybook/react";
import Link from "next/link";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { BadgeContainer } from "../../../stories/utils";
import { cn } from "../../lib/utils";
import { Badge } from "./badge";
const meta = {
title: "Shadcn/Table",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
return (
<div className="flex min-w-0 flex-col gap-6 px-4 py-6 lg:mx-auto lg:max-w-[1000px]">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<BadgeContainer label="Normal">
<TableDemo />
</BadgeContainer>
+32 -21
View File
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
import { useCallback, useRef, useState } from "react";
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
import { Button } from "./button";
import { ToolTipLabel } from "./tooltip";
export type TabLink = {
name: string;
@@ -20,6 +21,7 @@ export function TabLinks(props: {
className?: string;
tabContainerClassName?: string;
shadowColor?: string;
scrollableClassName?: string;
}) {
const { containerRef, lineRef, activeTabRef } =
useUnderline<HTMLAnchorElement>();
@@ -30,7 +32,7 @@ export function TabLinks(props: {
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
<ScrollShadow
scrollableClassName="pb-[8px] relative"
scrollableClassName={cn("pb-[8px] relative", props.scrollableClassName)}
shadowColor={props.shadowColor}
>
<div
@@ -81,6 +83,7 @@ export function TabButtons(props: {
isActive: boolean;
isDisabled?: boolean;
icon?: React.FC<{ className?: string }>;
toolTip?: string;
}[];
tabClassName?: string;
activeTabClassName?: string;
@@ -110,26 +113,31 @@ export function TabButtons(props: {
>
{props.tabs.map((tab, index) => {
return (
<Button
// biome-ignore lint/suspicious/noArrayIndexKey: tabs don't change order, so index is stable
key={index}
variant="ghost"
ref={tab.isActive ? activeTabRef : undefined}
className={cn(
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
!tab.isActive &&
"text-muted-foreground hover:text-foreground",
tab.isDisabled && "cursor-not-allowed opacity-50",
props.tabClassName,
tab.isActive && props.activeTabClassName,
)}
onClick={!tab.isDisabled ? tab.onClick : undefined}
<ToolTipLabel
key={typeof tab.name === "string" ? tab.name : index}
label={tab.toolTip}
>
{tab.icon && (
<tab.icon className={cn("size-6", props.tabIconClassName)} />
)}
{tab.name}
</Button>
<Button
variant="ghost"
ref={tab.isActive ? activeTabRef : undefined}
className={cn(
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
!tab.isActive &&
"text-muted-foreground hover:text-foreground",
tab.isDisabled && "cursor-not-allowed opacity-50",
props.tabClassName,
tab.isActive && props.activeTabClassName,
)}
onClick={!tab.isDisabled ? tab.onClick : undefined}
>
{tab.icon && (
<tab.icon
className={cn("size-6", props.tabIconClassName)}
/>
)}
{tab.name}
</Button>
</ToolTipLabel>
);
})}
</div>
@@ -159,9 +167,11 @@ function useUnderline<El extends HTMLElement>() {
const containerRect = containerRef.current.getBoundingClientRect();
const lineEl = lineRef.current;
const rect = activeTabEl.getBoundingClientRect();
const containerPaddingLeft =
containerRect.left - containerRef.current.offsetLeft;
lineEl.style.width = `${rect.width}px`;
lineEl.style.transform = `translateX(${
rect.left - containerRect.left
rect.left - containerPaddingLeft
}px)`;
setTimeout(() => {
lineEl.style.transition = "transform 0.3s, width 0.3s";
@@ -205,6 +215,7 @@ export function TabPathLinks(props: {
className?: string;
tabContainerClassName?: string;
shadowColor?: string;
scrollableClassName?: string;
}) {
const pathname = usePathname() || "";
const { links, ...restProps } = props;
+2
View File
@@ -3,6 +3,8 @@ export type ProductSKU =
| "plan:starter"
| "plan:growth"
| "plan:custom"
| "plan:accelerate"
| "plan:scale"
| "product:ecosystem_wallets"
| "product:engine_standard"
| "product:engine_premium"
@@ -15,8 +15,6 @@ import invariant from "tiny-invariant";
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
import { engineKeys } from "../cache-keys";
export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE";
// Engine instances
export type EngineInstance = {
id: string;
@@ -24,6 +22,8 @@ export type EngineInstance = {
name: string;
url: string;
lastAccessedAt: string;
isCloudHosted: boolean;
isPlanEngine: boolean;
status:
| "active"
| "pending"
@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
<div className="group relative w-full">
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
<Input
placeholder="Search by token address or chain ID"
placeholder="Search with anything!"
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
defaultValue={searchParams?.get("query") || ""}
onChange={(e) => handleSearch(e.target.value)}
@@ -7,6 +7,7 @@ import {
TableRow,
} from "@/components/ui/table";
import type { Address } from "thirdweb";
import { checksumAddress } from "thirdweb/utils";
import { getRoutes } from "../../../utils";
import { ChainlistPagination } from "../client/pagination";
import { RouteListCard } from "../server/routelist-card";
@@ -34,28 +35,83 @@ async function getRoutesToRender(params: SearchParams) {
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
originTextQuery?: string;
destinationTextQuery?: string;
}> = {};
if (params.type === "origin" || typeof params.type === "undefined") {
if (params.query?.startsWith("0x")) {
filters.originTokenAddress = params.query as Address;
} else if (params.query) {
} else if (Number.isInteger(Number(params.query))) {
filters.originChainId = Number(params.query);
} else if (params.query) {
filters.originTextQuery = params.query;
}
} else if (params.type === "destination") {
if (params.query?.startsWith("0x")) {
filters.destinationTokenAddress = params.query as Address;
} else if (params.query) {
} else if (Number.isInteger(Number(params.query))) {
filters.destinationChainId = Number(params.query);
} else if (params.query) {
filters.destinationTextQuery = params.query;
}
}
// Temporary, will update this after the /routes endpoint
filters.limit = 50_000;
const routes = await getRoutes(filters);
let routes = await getRoutes({ limit: 100_000 });
const totalCount = routes.length;
if (filters.originChainId) {
routes = routes.filter(
(route) => route.originToken.chainId === filters.originChainId,
);
}
if (filters.originTokenAddress) {
const originTokenAddress = filters.originTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.originToken.address) ===
checksumAddress(originTokenAddress),
);
}
if (filters.destinationChainId) {
routes = routes.filter(
(route) => route.destinationToken.chainId === filters.destinationChainId,
);
}
if (filters.destinationTokenAddress) {
const destinationTokenAddress = filters.destinationTokenAddress;
routes = routes.filter(
(route) =>
checksumAddress(route.destinationToken.address) ===
checksumAddress(destinationTokenAddress),
);
}
if (filters.originTextQuery) {
const originTextQuery = filters.originTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.originToken.name.toLowerCase().includes(originTextQuery) ||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
);
});
}
if (filters.destinationTextQuery) {
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
routes = routes.filter((route) => {
return (
route.destinationToken.name
.toLowerCase()
.includes(destinationTextQuery) ||
route.destinationToken.symbol
.toLowerCase()
.includes(destinationTextQuery)
);
});
}
return {
routesToRender: routes,
totalCount,
@@ -154,13 +210,21 @@ export async function RoutesData(props: {
out of{" "}
{filteredCount !== totalCount ? (
<>
<span className="text-accent-foreground">{filteredCount}</span>{" "}
<span className="text-accent-foreground">
{filteredCount.toLocaleString()}
</span>{" "}
routes that match filters. (Total:{" "}
<span className="text-accent-foreground">{totalCount}</span>)
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>
)
</>
) : (
<>
<span className="text-accent-foreground">{totalCount}</span> routes.
<span className="text-accent-foreground">
{totalCount.toLocaleString()}
</span>{" "}
routes.
</>
)}
</p>
@@ -2,43 +2,18 @@ import "server-only";
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
import { getAuthToken } from "app/api/lib/getAuthToken";
import type { Address } from "thirdweb";
import type { Route } from "./types/route";
export async function getRoutes({
limit,
offset,
originChainId,
originTokenAddress,
destinationChainId,
destinationTokenAddress,
}: {
limit?: number;
offset?: number;
originChainId?: number;
originTokenAddress?: Address;
destinationChainId?: number;
destinationTokenAddress?: Address;
} = {}) {
const url = new URL(`${BRIDGE_URL}/v1/routes`);
if (limit) {
url.searchParams.set("limit", limit.toString());
}
if (offset) {
url.searchParams.set("offset", offset.toString());
}
if (originChainId) {
url.searchParams.set("originChainId", originChainId.toString());
}
if (originTokenAddress) {
url.searchParams.set("originTokenAddress", originTokenAddress);
}
if (destinationChainId) {
url.searchParams.set("destinationChainId", destinationChainId.toString());
}
if (destinationTokenAddress) {
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
}
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
const token = await getAuthToken();
const routesResponse = await fetch(url, {
headers: token
@@ -17,7 +17,9 @@ export const ContractDirectListingsPage: React.FC<
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
<p className="text-lg">Contract Listings</p>
<h2 className="font-semibold text-2xl tracking-tight">
Direct Listings
</h2>
<div className="flex flex-row gap-4">
<CreateListingButton
isInsightSupported={isInsightSupported}
@@ -17,7 +17,9 @@ export const ContractEnglishAuctionsPage: React.FC<
return (
<div className="flex flex-col gap-6">
<div className="flex flex-row items-center justify-between">
<p className="text-lg">Contract Auctions</p>
<h2 className="font-semibold text-2xl tracking-tight">
English Auctions
</h2>
<div className="flex flex-row gap-4">
<CreateListingButton
contract={contract}
@@ -1,14 +1,7 @@
import {
Flex,
GridItem,
SimpleGrid,
Skeleton,
SkeletonText,
useBreakpointValue,
} from "@chakra-ui/react";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { useMemo } from "react";
import { type NFT, ZERO_ADDRESS } from "thirdweb";
import { Card, TrackedLink, type TrackedLinkProps } from "tw-components";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
type NFTWithContract = NFT & { contractAddress: string; chainId: number };
@@ -20,7 +13,8 @@ const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({
tokenURI: `1-0x123-${idx}`,
metadata: {
name: "Loading...",
description: "lorem ipsum loading sit amet",
description:
"lorem ipsum loading sit amet consectetur adipisicing elit. Quisquam, quos.",
id: BigInt(idx || 0),
uri: `1-0x123-${idx}`,
},
@@ -31,7 +25,7 @@ const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({
interface NFTCardsProps {
nfts: Array<NFTWithContract>;
trackingCategory: TrackedLinkProps["category"];
trackingCategory: string;
isPending: boolean;
allNfts?: boolean;
}
@@ -42,61 +36,82 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
isPending,
allNfts,
}) => {
const isMobile = useBreakpointValue({ base: true, md: false });
const dummyData = useMemo(() => {
return Array.from({
length: allNfts ? nfts.length : isMobile ? 2 : 3,
length: allNfts ? nfts.length : 3,
}).map((_, idx) => dummyMetadata(idx));
}, [nfts.length, isMobile, allNfts]);
}, [nfts.length, allNfts]);
return (
<SimpleGrid
gap={{ base: 3, md: 6 }}
columns={allNfts ? { base: 2, md: 4 } : { base: 2, md: 3 }}
>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
{(isPending ? dummyData : nfts).map((token) => {
const tokenId = token.id.toString();
return (
<GridItem
<div
key={`${token.chainId}_${token.contractAddress}_${tokenId}`}
as={TrackedLink}
category={trackingCategory}
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
_hover={{ opacity: 0.75, textDecoration: "none" }}
className="hover:-translate-y-0.5 relative flex h-full cursor-pointer flex-col overflow-hidden rounded-lg bg-background duration-200 hover:scale-[1.01]"
>
<Card p={0} h="full">
<div className="relative aspect-square w-full overflow-hidden rounded-xl">
<Skeleton isLoaded={!isPending}>
<NFTMediaWithEmptyState
metadata={token.metadata}
requireInteraction
width="100%"
height="100%"
/>
</Skeleton>
</div>
<Flex p={4} pb={3} gap={1} direction="column">
<Skeleton w={!isPending ? "100%" : "50%"} isLoaded={!isPending}>
<h2 className="font-semibold tracking-tight">
{token.metadata.name}
</h2>
</Skeleton>
<SkeletonText isLoaded={!isPending}>
<p className="line-clamp-3 text-muted-foreground text-sm">
{token.metadata.description ? (
token.metadata.description
) : (
<i>No description</i>
)}
</p>
</SkeletonText>
</Flex>
</Card>
</GridItem>
{/* border */}
<div className="absolute inset-0 rounded-lg border border-border" />
{/* image */}
<div className="relative aspect-square w-full overflow-hidden">
<SkeletonContainer
skeletonData={token.metadata}
loadedData={isPending ? undefined : token.metadata}
className="h-full w-full rounded-lg"
render={(v) => {
return (
<NFTMediaWithEmptyState
metadata={v}
requireInteraction
width="100%"
height="100%"
/>
);
}}
/>
</div>
<div className="p-4">
{/* title */}
<SkeletonContainer
skeletonData={token.metadata}
loadedData={isPending ? undefined : token.metadata}
className="mb-2"
render={(v) => {
return (
<h2 className="font-semibold tracking-tight">
<TrackedLinkTW
category={trackingCategory}
label="view_nft"
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
className="before:absolute before:inset-0"
>
{v.name}
</TrackedLinkTW>
</h2>
);
}}
/>
{/* Description */}
<SkeletonContainer
skeletonData={token.metadata}
loadedData={isPending ? undefined : token.metadata}
render={(v) => {
return (
<p className="line-clamp-3 text-muted-foreground text-sm">
{v.description ? v.description : <i>No description</i>}
</p>
);
}}
/>
</div>
</div>
);
})}
</SimpleGrid>
</div>
);
};
@@ -55,7 +55,7 @@ type Options =
};
export async function getClaimPhasesInLegacyFormat(
options: BaseTransactionOptions<Options>,
options: BaseTransactionOptions<Options> & { isMultiPhase: boolean },
): Promise<CombinedClaimCondition[]> {
const conditions = await (async () => {
switch (options.type) {
@@ -64,7 +64,10 @@ export async function getClaimPhasesInLegacyFormat(
case "erc721":
return ERC721Ext.getClaimConditions(options);
case "erc1155":
return ERC1155Ext.getClaimConditions(options);
return ERC1155Ext.getClaimConditions({
...options,
singlePhaseDrop: !options.isMultiPhase,
});
}
})();
@@ -126,7 +129,7 @@ export async function getClaimPhasesInLegacyFormat(
type PhaseInput = z.input<typeof LegacyClaimConditionInputSchema>;
export function setClaimPhasesTx(
baseOptions: BaseTransactionOptions<Options>,
baseOptions: BaseTransactionOptions<Options> & { isSinglePhase?: boolean },
rawPhases: PhaseInput[],
) {
const phases = rawPhases.map((phase) => {
@@ -172,6 +175,7 @@ export function setClaimPhasesTx(
contract: baseOptions.contract,
phases,
tokenId: baseOptions.tokenId,
singlePhaseDrop: baseOptions.isSinglePhase,
});
}
}
@@ -222,11 +222,15 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
const claimConditionsQuery = useReadContract(getClaimPhasesInLegacyFormat, {
contract,
isMultiPhase,
...(isErc20
? { type: "erc20", decimals: tokenDecimals.data }
: isErc721
? { type: "erc721" }
: { type: "erc1155", tokenId: BigInt(tokenId || 0) }),
: {
type: "erc1155",
tokenId: BigInt(tokenId || 0),
}),
});
const transformedQueryData = useMemo(() => {
@@ -370,6 +374,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
const tx = setClaimPhasesTx(
{
contract,
isSinglePhase: !isMultiPhase,
...(isErc20
? { type: "erc20", decimals: tokenDecimals.data }
: isErc721
@@ -635,6 +640,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
isErc20={isErc20}
contract={contract}
tokenId={tokenId}
isMultiphase={isMultiPhase}
/>
)}
</div>
@@ -10,6 +10,7 @@ interface ClaimConditionsProps {
isColumn?: true;
isERC20: boolean;
twAccount: Account | undefined;
isMultiphase: boolean;
}
export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
contract,
@@ -17,6 +18,7 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
isColumn,
isERC20,
twAccount,
isMultiphase,
}) => {
return (
<div className="flex w-full flex-col gap-6">
@@ -37,8 +39,7 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
contract={contract}
tokenId={tokenId}
isColumn={isColumn}
// always multi phase!
isMultiPhase={true}
isMultiPhase={isMultiphase}
/>
</div>
);
@@ -18,6 +18,7 @@ interface ResetClaimEligibilityProps {
contract: ThirdwebContract;
tokenId?: string;
twAccount: Account | undefined;
isMultiphase: boolean;
}
export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
@@ -25,6 +26,7 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
tokenId,
isErc20,
twAccount,
isMultiphase,
}) => {
const trackEvent = useTrack();
@@ -55,6 +57,7 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
return ERC1155Ext.resetClaimEligibility({
contract,
tokenId: BigInt(tokenId),
singlePhaseDrop: !isMultiphase,
});
}
// assume erc 721
@@ -36,33 +36,34 @@ export function ContractPageLayout(props: {
return (
<ChakraProviderSetup>
<SidebarLayout sidebarLinks={sidebarLinks}>
<div className="border-border border-b pb-8">
<div className="flex flex-col gap-4 ">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata
contract={contract}
chain={chainMetadata}
contractMetadata={dashboardContractMetadata}
externalLinks={externalLinks}
/>
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
teamsAndProjects={teamsAndProjects}
client={client}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
<div className="border-border border-b py-8">
<div className="container flex flex-col gap-4">
<div className="flex flex-col justify-between gap-4 md:flex-row">
<ContractMetadata
contract={contract}
chain={chainMetadata}
contractMetadata={dashboardContractMetadata}
externalLinks={externalLinks}
/>
<PrimaryDashboardButton
contractAddress={contract.address}
chain={contract.chain}
contractInfo={{
chain: chainMetadata,
chainSlug: chainMetadata.slug,
contractAddress: contract.address,
}}
teamsAndProjects={teamsAndProjects}
client={client}
/>
</div>
<DeprecatedAlert chain={chainMetadata} />
</div>
<div className="h-8" />
<div className="pb-10">{props.children}</div>
</div>
<SidebarLayout sidebarLinks={sidebarLinks}>
{props.children}
<div className="h-20" />
</SidebarLayout>
</ChakraProviderSetup>
);
@@ -66,7 +66,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
</div>
)}
<div className="flex grow flex-col gap-4">
<div className="flex grow flex-col gap-3">
{/* Title */}
{!data ? (
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
@@ -75,7 +75,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
) : (
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
{data?.name && (
<h1 className="line-clamp-2 font-semibold text-2xl tracking-tight lg:text-3xl">
<h1 className="line-clamp-2 font-bold text-2xl tracking-tight lg:text-3xl">
{data.name}
</h1>
)}
@@ -89,7 +89,7 @@ function AddToProjectButton(props: {
return (
<Dialog>
<DialogTrigger asChild>
<Button className="gap-2" variant="outline">
<Button className="gap-2 bg-card" variant="outline">
<PlusIcon className="size-4" />
Add To Project
</Button>
@@ -1,6 +1,6 @@
"use client";
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
import { Skeleton, Stat, StatLabel, StatNumber } from "@chakra-ui/react";
import { SkeletonContainer } from "@/components/ui/skeleton";
import type { UseQueryResult } from "@tanstack/react-query";
import {
type AnalyticsQueryParams,
@@ -14,9 +14,9 @@ import {
useTotalContractTransactionAnalytics,
useTotalContractUniqueWallets,
} from "data/analytics/hooks";
import { Suspense, useMemo, useState } from "react";
import { formatDate } from "date-fns";
import { useMemo, useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { Card } from "tw-components";
import {
DateRangeSelector,
type Range,
@@ -121,6 +121,15 @@ type ChartProps = {
startDate: Date;
endDate: Date;
};
function toolTipLabelFormatter(_v: string, item: unknown) {
if (Array.isArray(item)) {
const time = item[0].payload.time as number;
return formatDate(new Date(time), "MMM d, yyyy");
}
return undefined;
}
function UniqueWalletsChart(props: ChartProps) {
const analyticsQuery = useContractUniqueWalletAnalytics(props);
@@ -128,6 +137,7 @@ function UniqueWalletsChart(props: ChartProps) {
<ThirdwebBarChart
header={{
title: "Unique Wallets",
titleClassName: "mb-0.5 text-xl",
description:
"The number of unique wallet addresses that have sent a transaction to this contract.",
}}
@@ -140,6 +150,8 @@ function UniqueWalletsChart(props: ChartProps) {
},
}}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -156,6 +168,7 @@ function TotalTransactionsChart(props: ChartProps) {
<ThirdwebBarChart
header={{
title: "Total Transactions",
titleClassName: "mb-0.5 text-xl",
description:
"The number of transactions that have been sent to this contract.",
}}
@@ -168,6 +181,8 @@ function TotalTransactionsChart(props: ChartProps) {
},
}}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -179,6 +194,7 @@ function TotalEventsChart(props: ChartProps) {
<ThirdwebBarChart
header={{
title: "Total Events",
titleClassName: "mb-0.5 text-xl",
description:
"The number of on-chain events that have been emitted from this contract.",
}}
@@ -191,6 +207,8 @@ function TotalEventsChart(props: ChartProps) {
},
}}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -227,6 +245,7 @@ function FunctionBreakdownChart(
<ThirdwebBarChart
header={{
title: "Function Breakdown",
titleClassName: "mb-0.5 text-xl",
description:
"The breakdown of calls to each write function from transactions.",
}}
@@ -247,6 +266,8 @@ function FunctionBreakdownChart(
)}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
showLegend
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -283,6 +304,7 @@ function EventBreakdownChart(
<ThirdwebBarChart
header={{
title: "Event Breakdown",
titleClassName: "mb-0.5 text-xl",
description: "The breakdown of events emitted by this contract.",
}}
data={mappedQueryData || []}
@@ -302,6 +324,8 @@ function EventBreakdownChart(
)}
chartClassName="aspect-[1.5] lg:aspect-[4.5]"
showLegend
hideLabel={false}
toolTipLabelFormatter={toolTipLabelFormatter}
/>
);
}
@@ -320,25 +344,12 @@ const AnalyticsStat: React.FC<AnalyticsStatProps> = ({
useTotal,
}) => {
return (
<Suspense fallback={<AnalyticsSkeleton label={label} />}>
<AnalyticsData
chainId={chainId}
contractAddress={contractAddress}
useTotal={useTotal}
label={label}
/>
</Suspense>
);
};
const AnalyticsSkeleton: React.FC<{ label: string }> = ({ label }) => {
return (
<Card as={Stat} className="bg-card">
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
<Skeleton isLoaded={false}>
<StatNumber>{0}</StatNumber>
</Skeleton>
</Card>
<AnalyticsData
chainId={chainId}
contractAddress={contractAddress}
useTotal={useTotal}
label={label}
/>
);
};
@@ -355,20 +366,23 @@ const AnalyticsData: React.FC<AnalyticsStatProps> = ({
chainId,
});
const data = useMemo(() => {
if (!totalQuery.data) {
return 0;
}
return totalQuery.data.count;
}, [totalQuery.data]);
return (
<Card as={Stat} className="bg-card">
<StatLabel mb={{ base: 1, md: 0 }}>{label}</StatLabel>
<Skeleton isLoaded={totalQuery.isFetched}>
<StatNumber>{data.toLocaleString()}</StatNumber>
</Skeleton>
</Card>
);
return <AnalyticsStatUI label={label} data={totalQuery.data?.count} />;
};
function AnalyticsStatUI(props: {
label: string;
data: number | undefined;
}) {
return (
<dl className="rounded-lg border bg-card p-4">
<dt className="font-semibold">{props.label}</dt>
<SkeletonContainer
skeletonData={10000}
loadedData={props.data}
render={(v) => {
return <dd className="font-normal text-xl">{v.toLocaleString()}</dd>;
}}
/>
</dl>
);
}
@@ -36,6 +36,7 @@ export function ClaimConditionsClient(props: {
contract={props.contract}
isERC20={supportedERCs.isERC20}
twAccount={props.twAccount}
isMultiphase={true}
/>
);
}
@@ -41,6 +41,7 @@ export default async function Page(props: {
contract={contract}
isERC20={supportedERCs.isERC20}
twAccount={account}
isMultiphase={true}
/>
);
}
@@ -3,14 +3,11 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ZERO_ADDRESS } from "thirdweb";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import {
ErrorProvider,
type TransactionError,
} from "../../../../../../../contexts/error-handler";
import type { TransactionError } from "../../../../../../../contexts/error-handler";
import { accountStub } from "../../../../../../../stories/stubs";
import {
BatchMetadataModuleUI,
@@ -21,7 +18,6 @@ const meta = {
title: "Modules/BatchMetadata",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -31,17 +27,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
@@ -80,38 +69,35 @@ function Component() {
return (
<ThirdwebProvider>
<ErrorProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
<div className="flex items-center gap-5">
<CheckboxWithLabel
value={isOwner}
onChange={setIsOwner}
id="isOwner"
label="Is Owner"
/>
</div>
<BadgeContainer label="Default">
<BatchMetadataModuleUI
twAccount={accountStub()}
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uploadMetadata={uploadMetadataStub}
uninstallButton={{
onClick: async () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={isOwner}
contractChainId={1}
/>
</BadgeContainer>
<Toaster richColors />
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
</ErrorProvider>
<div className="flex items-center gap-5">
<CheckboxWithLabel
value={isOwner}
onChange={setIsOwner}
id="isOwner"
label="Is Owner"
/>
</div>
<BadgeContainer label="Default">
<BatchMetadataModuleUI
twAccount={accountStub()}
contractInfo={contractInfo}
moduleAddress="0x0000000000000000000000000000000000000000"
uploadMetadata={uploadMetadataStub}
uninstallButton={{
onClick: async () => removeMutation.mutateAsync(),
isPending: removeMutation.isPending,
}}
isOwnerAccount={isOwner}
contractChainId={1}
/>
</BadgeContainer>
</div>
</ThirdwebProvider>
);
}
@@ -11,8 +11,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { subDays } from "date-fns";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { NATIVE_TOKEN_ADDRESS, ZERO_ADDRESS } from "thirdweb";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { checksumAddress } from "thirdweb/utils";
@@ -29,7 +28,6 @@ const meta = {
title: "Modules/Claimable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -39,17 +37,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
const claimCondition = {
@@ -67,7 +58,7 @@ const claimCondition = {
function Component() {
const [isOwner, setIsOwner] = useState(true);
const [name, setName] = useState("ClaimableERC721");
const [name, setName] = useState("MintableERC721");
const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false);
const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] =
useState(false);
@@ -110,7 +101,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -196,8 +187,6 @@ function Component() {
isValidTokenId={true}
noClaimConditionSet={noClaimConditionSet}
/>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -11,8 +11,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -25,7 +25,6 @@ const meta = {
title: "Modules/Mintable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -35,17 +34,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -84,7 +76,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -175,8 +167,6 @@ function Component() {
contractChainId={1}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -2,33 +2,22 @@ import { Checkbox } from "@/components/ui/checkbox";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { BadgeContainer } from "stories/utils";
import { ThirdwebProvider } from "thirdweb/react";
import { ModuleCardUI } from "./module-card";
const meta = {
title: "Modules/ModuleCard",
component: Component,
parameters: {
layout: "centered",
},
} satisfies Meta<typeof Component>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
@@ -120,8 +109,6 @@ function Component() {
</div>
</ModuleCardUI>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -16,7 +16,6 @@ const meta = {
title: "Modules/OpenEditionMetadata",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -26,17 +25,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
function Component() {
const [isOwner, setIsOwner] = useState(true);
async function setSharedMetadataStub(values: SetSharedMetadataFormValues) {
@@ -98,8 +90,6 @@ function Component() {
twAccount={accountStub()}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -18,7 +18,6 @@ const meta = {
title: "Modules/Royalty",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -28,17 +27,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const _testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -79,7 +71,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div>
<ConnectButton client={getThirdwebClient()} />
</div>
@@ -168,8 +160,6 @@ function Component() {
twAccount={twAccount}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -3,8 +3,8 @@ import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useMutation } from "@tanstack/react-query";
import { useState } from "react";
import { Toaster, toast } from "sonner";
import { BadgeContainer, mobileViewport } from "stories/utils";
import { toast } from "sonner";
import { BadgeContainer } from "stories/utils";
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
import { accountStub } from "../../../../../../../stories/stubs";
import {
@@ -16,7 +16,6 @@ const meta = {
title: "Modules/Transferable",
component: Component,
parameters: {
layout: "centered",
nextjs: {
appDirectory: true,
},
@@ -26,17 +25,10 @@ const meta = {
export default meta;
type Story = StoryObj<typeof meta>;
export const Desktop: Story = {
export const Variants: Story = {
args: {},
};
export const Mobile: Story = {
args: {},
parameters: {
viewport: mobileViewport("iphone14"),
},
};
const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
function Component() {
@@ -65,7 +57,7 @@ function Component() {
return (
<ThirdwebProvider>
<div className="container flex max-w-[1150px] flex-col gap-10 py-10">
<div className="container flex max-w-6xl flex-col gap-10 py-10">
<div className="flex gap-2">
<Checkbox
id="terms1"
@@ -139,8 +131,6 @@ function Component() {
twAccount={accountStub()}
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -60,7 +60,7 @@ export const ContractNFTPage: React.FC<NftOverviewPageProps> = ({
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
<h2 className="font-semibold text-2xl tracking-tight">Contract NFTs</h2>
<h2 className="font-semibold text-2xl tracking-tight">NFTs</h2>
<div className="grid grid-cols-2 gap-2 md:flex">
{isRevealable && (
<NFTRevealButton contract={contract} twAccount={twAccount} />
@@ -4,33 +4,31 @@ import { UnexpectedValueErrorMessage } from "@/components/blocks/error-fallbacks
import { WalletAddress } from "@/components/blocks/wallet-address";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import { TabButtons } from "@/components/ui/tabs";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
import {
Box,
ButtonGroup,
Divider,
Flex,
GridItem,
IconButton,
SimpleGrid,
Tooltip,
useBreakpointValue,
} from "@chakra-ui/react";
import { useChainSlug } from "hooks/chains/chainSlug";
import { ChevronLeftIcon, ExternalLinkIcon } from "lucide-react";
import { ExternalLinkIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import type { NFT, ThirdwebClient, ThirdwebContract } from "thirdweb";
import { getNFT as getErc721NFT } from "thirdweb/extensions/erc721";
import { getNFT as getErc1155NFT } from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import { Button, Card } from "tw-components";
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
import { shortenString } from "utils/usedapp-external";
import { NftProperty } from "../components/nft-property";
import { useNFTDrawerTabs } from "./useNftDrawerTabs";
@@ -66,7 +64,6 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
twAccount,
}) => {
const [tab, setTab] = useState("Details");
const isMobile = useBreakpointValue({ base: true, md: false });
const router = useDashboardRouter();
const chainId = contract.chain.id;
const chainSlug = useChainSlug(chainId || 1);
@@ -89,15 +86,6 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
},
);
const tokenURIHttpLink = resolveSchemeWithErrorHandler({
client,
uri: nft?.tokenURI,
});
const nftImageLink = resolveSchemeWithErrorHandler({
client,
uri: nft?.metadata.image,
});
if (isPending) {
return (
<div className="flex h-[400px] items-center justify-center">
@@ -123,230 +111,218 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
: undefined,
};
return (
<div>
<div className="flex flex-col gap-6 lg:flex-row">
<div className="relative aspect-square w-full shrink-0 self-start overflow-hidden bg-card lg:w-[350px]">
{/* border */}
<div className="absolute inset-0 z-0 rounded-lg border" />
{/* media */}
<NFTMediaWithEmptyState metadata={nftMetadata} className="z-10" />
</div>
<div className="flex w-full flex-col gap-6">
{/* breadcrumb + title */}
<div>
<Breadcrumb className="mb-3">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href={`/${chainSlug}/${contract.address}/nfts`}
onClick={(e) => {
e.preventDefault();
router.push(`/${chainSlug}/${contract.address}/nfts`);
}}
>
NFTs
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink>#{tokenId}</BreadcrumbLink>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<div className="flex flex-col gap-1.5">
<NFTName value={nft.metadata.name} />
{nft.metadata?.description && (
<NFTDescription value={nft.metadata.description} />
)}
</div>
</div>
<TabButtons
tabClassName="!text-sm"
tabs={[
{
name: "Details",
onClick: () => setTab("Details"),
isActive: tab === "Details",
isDisabled: false,
},
...tabs.map((tb) => ({
name: tb.title,
onClick: () => setTab(tb.title),
isActive: tab === tb.title,
isDisabled: tb.isDisabled,
toolTip: tb.isDisabled ? tb.disabledText : undefined,
})),
].sort((a, b) => (a.isDisabled ? 1 : b.isDisabled ? -1 : 0))}
/>
{/* tab contents */}
{tab === "Details" && <NFTDetailsTab nft={nft} client={client} />}
{tabs.map((tb) => {
return (
tb.title === tab && (
<div key={tb.title} className="w-full">
{tb.children}
</div>
)
);
})}
</div>
</div>
</div>
);
};
function NFTDetailsTab(props: {
nft: NFT;
client: ThirdwebClient;
}) {
const { nft, client } = props;
const properties = nft.metadata.attributes || nft.metadata.properties;
return (
<Flex flexDir={{ base: "column", lg: "row" }} gap={6}>
<Card h="full" position="relative" minH="100px">
<Box w="50px" position="absolute" zIndex={1000} top={6} left={6}>
{/* TODO - replace this with breadcrumbs */}
<Card p={1} bgColor="backgroundCardHighlight">
<IconButton
w="inherit"
variant="ghost"
onClick={() =>
router.push(`/${chainSlug}/${contract.address}/nfts`)
<div className="flex flex-col gap-8">
{/* common */}
<div className="flex flex-col gap-3">
<div className="grid grid-cols-1 items-center gap-x-8 gap-y-4 lg:grid-cols-[1fr_2fr]">
{/* token id */}
<div>
<p className="mb-1 text-muted-foreground text-sm">Token ID</p>
<CopyTextButton
textToCopy={nft.id?.toString()}
textToShow={
nft.id?.toString().length > 8
? `${nft.id.toString().slice(0, 4)}...${nft.id.toString().slice(-4)}`
: nft.id?.toString()
}
aria-label="Back"
icon={<ChevronLeftIcon className="size-6" />}
>
Back
</IconButton>
</Card>
</Box>
<NFTMediaWithEmptyState
metadata={nftMetadata}
width={isMobile ? "100%" : "300px"}
height={isMobile ? "100%" : "300px"}
/>
</Card>
<Flex flexDir="column" gap={6} w="full" px={2}>
<Flex flexDir="column" gap={1.5}>
<NFTName value={nft.metadata.name} />
{nft.metadata?.description && (
<NFTDescription value={nft.metadata.description} />
tooltip="Token ID"
copyIconPosition="right"
className="min-w-16 justify-between bg-card"
/>
</div>
{/* owner */}
{nft.owner && (
<div>
<p className="mb-1 text-muted-foreground text-sm">Owner</p>
<WalletAddress
address={nft.owner}
className="h-auto rounded-full border bg-card px-2 py-1 pr-4 text-foreground text-sm"
/>
</div>
)}
</Flex>
<Flex flexDir="column" gap={{ base: 0, md: 4 }}>
<Box
w="full"
overflow={{ base: "auto", md: "hidden" }}
pb={{ base: 4, md: 0 }}
>
<ButtonGroup size="sm" variant="ghost" spacing={2}>
<Button
type="button"
isActive={tab === "Details"}
_active={{
bg: "bgBlack",
color: "bgWhite",
}}
rounded="lg"
onClick={() => setTab("Details")}
{/* type */}
<div>
<p className="mb-1 text-muted-foreground text-sm">Token Standard</p>
<Badge
variant="outline"
className="bg-card py-1 font-normal text-sm"
>
{nft.type}
</Badge>
</div>
{/* supply */}
{nft.type !== "ERC721" && (
<div className="w-full">
<p className="mb-1 text-muted-foreground text-sm">Supply</p>
<ToolTipLabel
label={nft.supply.toLocaleString("en-US")}
contentClassName="text-sm break-all"
>
Details
</Button>
{tabs.map((tb) => (
<Tooltip
key={tb.title}
p={0}
bg="transparent"
boxShadow="none"
label={
tb.isDisabled ? (
<Card py={2} px={4} bgColor="backgroundHighlight">
<p>{tb.disabledText}</p>
</Card>
) : (
""
)
}
>
<Button
isDisabled={tb.isDisabled}
type="button"
isActive={tab === tb.title}
_active={{
bg: "bgBlack",
color: "bgWhite",
}}
rounded="lg"
onClick={() => setTab(tb.title)}
>
{tb.title}
</Button>
</Tooltip>
))}
</ButtonGroup>
</Box>
<Divider />
</Flex>
<p className="max-w-[200px] truncate text-sm">
{nft.supply.toLocaleString("en-US")}
</p>
</ToolTipLabel>
</div>
)}
{tab === "Details" && (
<Flex flexDir="column" gap={4}>
<Card as={Flex} flexDir="column" gap={3}>
<SimpleGrid rowGap={3} columns={12} placeItems="center left">
<GridItem colSpan={4}>
<p className="font-semibold">Token ID</p>
</GridItem>
<GridItem colSpan={8}>
<CopyTextButton
textToCopy={nft.id?.toString()}
textToShow={
nft.id?.toString().length > 8
? `${nft.id.toString().slice(0, 4)}...${nft.id.toString().slice(-4)}`
: nft.id?.toString()
}
tooltip="Token ID"
copyIconPosition="right"
/>
</GridItem>
{/* token uri */}
<div>
<p className="mb-1 text-muted-foreground text-sm">Token URI</p>
<IPFSLinkGroup
ipfsLink={nft.tokenURI}
tooltip="The URI of this NFT"
client={client}
httpsLinkTooltip="View URI"
/>
</div>
{nft.owner && (
<>
<GridItem colSpan={4}>
<p className="font-semibold">Owner</p>
</GridItem>
<GridItem colSpan={8}>
<WalletAddress address={nft.owner} />
</GridItem>
</>
)}
<GridItem colSpan={4}>
<p className="font-semibold">Token Standard</p>
</GridItem>
<GridItem colSpan={8}>{nft.type}</GridItem>
{nft.type !== "ERC721" && (
<>
<GridItem colSpan={4}>
<p className="font-semibold">Supply</p>
</GridItem>
<GridItem colSpan={8}>
<p>{nft.supply.toLocaleString("en-US")}</p>
</GridItem>
</>
)}
<GridItem colSpan={4}>
<p className="font-semibold">Token URI</p>
</GridItem>
<GridItem
colSpan={8}
className="flex flex-row items-center gap-1"
>
<CopyTextButton
textToCopy={nft.tokenURI}
textToShow={shortenString(nft.tokenURI)}
tooltip="The URI of this NFT"
copyIconPosition="right"
/>
{tokenURIHttpLink && (
<Button variant="ghost" size="sm">
<Link href={tokenURIHttpLink} target="_blank">
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
)}
</GridItem>
{nft.metadata.image && (
<>
<GridItem colSpan={4}>
<p className="font-semibold">Media URI</p>
</GridItem>
<GridItem
colSpan={8}
className="flex flex-row items-center gap-1"
>
<CopyTextButton
textToCopy={nft.metadata.image}
textToShow={shortenString(nft.metadata.image)}
tooltip="The media URI of this NFT"
copyIconPosition="right"
/>
{nftImageLink && (
<Button variant="ghost" size="sm">
<Link href={nftImageLink} target="_blank">
<ExternalLinkIcon className="size-4" />
</Link>
</Button>
)}
</GridItem>
</>
)}
</SimpleGrid>
</Card>
{properties ? (
<Card as={Flex} flexDir="column" gap={4}>
<p className="font-semibold">Attributes</p>
{Array.isArray(properties) &&
String(properties[0]?.value) !== "undefined" ? (
<SimpleGrid columns={{ base: 2, md: 4 }} gap={2}>
{/* biome-ignore lint/suspicious/noExplicitAny: FIXME */}
{properties.map((property: any, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<NftProperty key={idx} property={property} />
))}
</SimpleGrid>
) : (
<CodeClient
code={JSON.stringify(properties, null, 2) || ""}
lang="json"
/>
)}
</Card>
) : null}
</Flex>
)}
{tabs.map((tb) => {
return (
tb.title === tab && (
<Flex key={tb.title} px={0} w="full">
{tb.children}
</Flex>
)
);
})}
</Flex>
</Flex>
{/* media uri */}
{nft.metadata.image && (
<div>
<p className="mb-1 text-muted-foreground text-sm">Media URI</p>
<IPFSLinkGroup
ipfsLink={nft.metadata.image}
tooltip="The media URI of this NFT"
client={client}
httpsLinkTooltip="View Media"
/>
</div>
)}
</div>
</div>
{/* attributes */}
{properties ? (
<div>
<h3 className="mb-2 flex items-center gap-2 font-medium text-base">
Attributes
{Array.isArray(properties) && properties.length > 0 && (
<Badge
variant="outline"
className="bg-card px-2 text-muted-foreground"
>
{properties.length}
</Badge>
)}
</h3>
<div>
{Array.isArray(properties) &&
String(properties[0]?.value) !== "undefined" ? (
<div className="grid grid-cols-2 gap-3 lg:grid-cols-3">
{/* biome-ignore lint/suspicious/noExplicitAny: FIXME */}
{properties.map((property: any, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<NftProperty key={idx} property={property} />
))}
</div>
) : (
<CodeClient
code={JSON.stringify(properties, null, 2) || ""}
lang="json"
/>
)}
</div>
</div>
) : null}
</div>
);
};
}
function NFTName(props: {
value: unknown;
}) {
if (typeof props.value === "string") {
return (
<h2 className="font-semibold text-lg tracking-tight"> {props.value}</h2>
<h2 className="font-bold text-3xl tracking-tight"> {props.value}</h2>
);
}
@@ -360,11 +336,49 @@ function NFTName(props: {
);
}
function IPFSLinkGroup(props: {
ipfsLink: string;
tooltip: string;
client: ThirdwebClient;
httpsLinkTooltip: string;
}) {
const httpLink = resolveSchemeWithErrorHandler({
client: props.client,
uri: props.ipfsLink,
});
return (
<div className="flex flex-row items-center gap-1.5">
<CopyTextButton
textToCopy={props.ipfsLink}
textToShow={props.ipfsLink}
tooltip={props.tooltip}
copyIconPosition="right"
className="max-w-[200px] truncate bg-card"
/>
{httpLink && (
<ToolTipLabel label={props.httpsLinkTooltip}>
<Button
variant="outline"
size="sm"
asChild
className="size-[34px] rounded-full bg-card p-0 text-muted-foreground"
>
<Link href={httpLink} target="_blank">
<ExternalLinkIcon className="size-3" />
</Link>
</Button>
</ToolTipLabel>
)}
</div>
);
}
function NFTDescription(props: {
value: unknown;
}) {
if (typeof props.value === "string") {
return <p className="text-muted-foreground"> {props.value}</p>;
return <p className="text-muted-foreground text-sm"> {props.value}</p>;
}
return (
@@ -68,7 +68,6 @@ export function useNFTDrawerTabs({
const hasERC1155ClaimConditions = (() => {
return [
// reads
ERC1155Ext.isGetClaimConditionByIdSupported(functionSelectors),
ERC1155Ext.isGetClaimConditionsSupported(functionSelectors),
ERC1155Ext.isGetActiveClaimConditionSupported(functionSelectors),
// writes
@@ -77,6 +76,10 @@ export function useNFTDrawerTabs({
].every(Boolean);
})();
const isERC1155Multiphase = (() => {
return ERC1155Ext.isGetClaimConditionByIdSupported(functionSelectors);
})();
const isBurnable = (() => {
if (isERC721) {
return ERC721Ext.isBurnSupported(functionSelectors);
@@ -130,6 +133,7 @@ export function useNFTDrawerTabs({
tokenId={tokenId}
isColumn
twAccount={twAccount}
isMultiphase={isERC1155Multiphase}
/>
),
},
@@ -1,5 +1,3 @@
import { Card } from "@/components/ui/card";
interface NftPropertyProps {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
property: any;
@@ -7,17 +5,17 @@ interface NftPropertyProps {
export const NftProperty: React.FC<NftPropertyProps> = ({ property }) => {
return (
<Card className="flex flex-col gap-2 p-3">
<div className="rounded-lg border bg-card p-3 text-sm">
{property?.trait_type && (
<p className="text-center text-link-foreground text-sm leading-[1.2]">
<p className="mb-0.5 text-muted-foreground text-sm">
{property?.trait_type}
</p>
)}
<p className="text-center text-muted-foreground text-sm">
<p className="text-foreground text-sm">
{typeof property?.value === "object"
? JSON.stringify(property?.value || {})
: property?.value}
</p>
</Card>
</div>
);
};
@@ -35,7 +35,7 @@ export const SupplyCards: React.FC<SupplyCardsProps> = ({ contract }) => {
);
return (
<div className="flex flex-row gap-3 md:gap-6 [&>*]:grow">
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
<StatCard
value={realTotalSupply.toString()}
label="Total Supply"
@@ -1,12 +1,12 @@
import type { ThirdwebContract } from "thirdweb";
import { AnalyticsOverview } from "./components/Analytics";
import { TokenDetailsCard } from "../tokens/components/supply";
import { ContractAnalyticsOverviewCard } from "./components/Analytics";
import { BuildYourApp } from "./components/BuildYourApp";
import { ContractChecklist } from "./components/ContractChecklist";
import { LatestEvents } from "./components/LatestEvents";
import { MarketplaceDetails } from "./components/MarketplaceDetails";
import { NFTDetails } from "./components/NFTDetails";
import { PermissionsTable } from "./components/PermissionsTable";
import { TokenDetails } from "./components/TokenDetails";
interface ContractOverviewPageProps {
contract: ThirdwebContract;
@@ -38,8 +38,8 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
publishedBy,
}) => {
return (
<div className="flex flex-col gap-8 lg:flex-row">
<div className="flex flex-col gap-16">
<div className="flex flex-col gap-10 lg:flex-row lg:gap-8">
<div className="flex grow flex-col gap-10 overflow-hidden">
<ContractChecklist
isErc721={isErc721}
isErc1155={isErc1155}
@@ -50,7 +50,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
/>
{isAnalyticsSupported && (
<AnalyticsOverview
<ContractAnalyticsOverviewCard
contractAddress={contract.address}
chainId={contract.chain.id}
trackingCategory={TRACKING_CATEGORY}
@@ -77,7 +77,7 @@ export const ContractOverviewPage: React.FC<ContractOverviewPageProps> = ({
/>
)}
{isErc20 && <TokenDetails contract={contract} />}
{isErc20 && <TokenDetailsCard contract={contract} />}
<LatestEvents
contract={contract}
@@ -12,19 +12,16 @@ import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import { useMemo, useState } from "react";
interface AnalyticsOverviewProps {
chainId: number;
contractAddress: string;
trackingCategory: string;
chainSlug: string;
function getDayKey(date: Date) {
return date.toISOString().split("T")[0];
}
export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
chainId,
contractAddress,
trackingCategory,
chainSlug,
}) => {
export function ContractAnalyticsOverviewCard(props: {
contractAddress: string;
chainId: number;
trackingCategory: string;
chainSlug: string;
}) {
const trackEvent = useTrack();
const [startDate] = useState(
(() => {
@@ -35,55 +32,27 @@ export const AnalyticsOverview: React.FC<AnalyticsOverviewProps> = ({
);
const [endDate] = useState(new Date());
return (
<div className="relative">
<div className="mb-4 flex items-center justify-between gap-4">
<h2 className="font-semibold text-2xl tracking-tight">Analytics</h2>
<Button
asChild
className="gap-1"
size="sm"
variant="outline"
onClick={() => {
trackEvent({
category: trackingCategory,
action: "click",
label: "view_all_analytics",
});
}}
>
<Link href={`/${chainSlug}/${contractAddress}/analytics`}>
<span>View All</span>
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
const wallets = useContractUniqueWalletAnalytics({
chainId: props.chainId,
contractAddress: props.contractAddress,
startDate,
endDate,
});
<OverviewAnalytics
chainId={chainId}
contractAddress={contractAddress}
endDate={endDate}
startDate={startDate}
/>
</div>
);
};
const transactions = useContractTransactionAnalytics({
chainId: props.chainId,
contractAddress: props.contractAddress,
startDate,
endDate,
});
type ChartProps = {
contractAddress: string;
chainId: number;
startDate: Date;
endDate: Date;
};
const events = useContractEventAnalytics({
chainId: props.chainId,
contractAddress: props.contractAddress,
startDate,
endDate,
});
function getDayKey(date: Date) {
return date.toISOString().split("T")[0];
}
function OverviewAnalytics(props: ChartProps) {
const wallets = useContractUniqueWalletAnalytics(props);
const transactions = useContractTransactionAnalytics(props);
const events = useContractEventAnalytics(props);
const isPending =
wallets.isPending || transactions.isPending || events.isPending;
@@ -135,7 +104,32 @@ function OverviewAnalytics(props: ChartProps) {
data={mergedData || []}
isPending={isPending}
showLegend
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
chartClassName="aspect-[1.5] lg:aspect-[3]"
customHeader={
<div className="flex items-center justify-between gap-4 border-b p-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">Analytics</h2>
<Button
asChild
className="gap-2 bg-background text-muted-foreground"
size="sm"
variant="outline"
onClick={() => {
trackEvent({
category: props.trackingCategory,
action: "click",
label: "view_all_analytics",
});
}}
>
<Link
href={`/${props.chainSlug}/${props.contractAddress}/analytics`}
>
<span>View All</span>
<ArrowRightIcon className="size-4" />
</Link>
</Button>
</div>
}
/>
);
}
@@ -1,79 +1,41 @@
"use client";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ReactIcon } from "../../../../../../../components/icons/brand-icons/ReactIcon";
import { TypeScriptIcon } from "../../../../../../../components/icons/brand-icons/TypeScriptIcon";
import {
Flex,
GridItem,
LinkBox,
LinkOverlay,
SimpleGrid,
} from "@chakra-ui/react";
import { ChakraNextImage as Image } from "components/Image";
import { PRODUCTS } from "components/product-pages/common/nav/data";
import { Card } from "tw-components/card";
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
import { Text } from "tw-components/text";
const RENDERED_PRODUCTS = ["sdk", "storage", "ui-components", "auth"];
interface BuildYourAppProps {
trackingCategory: TrackedLinkProps["category"];
export function BuildYourApp(props: {
trackingCategory: string;
contractAddress: string;
chainSlug: string;
}
export const BuildYourApp: React.FC<BuildYourAppProps> = ({
trackingCategory,
contractAddress,
chainSlug,
}) => {
}) {
return (
<Card
px={{ base: 4, md: 8 }}
py={{ base: 6, md: 8 }}
as={LinkBox}
transition="all 0.2s"
_hover={{ borderColor: "whiteAlpha.300" }}
>
<SimpleGrid {...{ columns: { base: 1, md: 2 } }} gap={8}>
<GridItem as={Flex} direction="column" gap={4}>
<h2 className="font-semibold text-2xl tracking-tight">
Build your app
</h2>
<Text size="body.md">
<LinkOverlay
as={TrackedLink}
category={trackingCategory}
label="build_your_app"
href={`/${chainSlug}/${contractAddress}/code`}
color="blue.500"
>
Learn more
</LinkOverlay>{" "}
about how you can use thirdweb tools to build apps on top of this
contract.
</Text>
</GridItem>
<GridItem as={Flex} align="center" justify="end" gap={3}>
{RENDERED_PRODUCTS.map((p) => {
const product = PRODUCTS.find((item) => item.label === p);
return (
product?.icon && (
<Flex
key={product.name}
rounded="full"
bg="#0E0E10"
align="center"
justify="center"
w={14}
h={14}
>
<Image boxSize={7} src={product.icon} alt={product.name} />
</Flex>
)
);
})}
</GridItem>
</SimpleGrid>
</Card>
<div className="relative flex flex-col justify-between gap-4 rounded-lg border bg-card p-6 hover:border-active-border lg:flex-row">
{/* left */}
<div className="max-w-sm">
<h2 className="mb-2 font-semibold text-xl leading-none tracking-tight">
Build your app
</h2>
<TrackedLinkTW
category={props.trackingCategory}
label="build_your_app"
href={`/${props.chainSlug}/${props.contractAddress}/code`}
className="block text-balance text-muted-foreground text-sm before:absolute before:inset-0"
>
Learn more about how you can use thirdweb tools to build apps on top
of this contract
</TrackedLinkTW>{" "}
</div>
{/* right */}
<div className="flex items-center justify-end gap-3">
{[TypeScriptIcon, ReactIcon].map((Icon, i) => {
return (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<div key={i} className="rounded-full border bg-background p-3">
<Icon className="size-5 text-muted-foreground" />
</div>
);
})}
</div>
</div>
);
};
}
@@ -0,0 +1,105 @@
import type { Meta, StoryObj } from "@storybook/react";
import { LatestEventsUI } from "./LatestEvents";
const meta: Meta<typeof LatestEventsUI> = {
title: "Contracts/Overview/LatestEvents",
component: LatestEventsUI,
decorators: [
(Story) => (
<div className="container max-w-4xl py-10">
<Story />
</div>
),
],
args: {
allEvents: [],
autoUpdate: false,
eventsHref: "/ethereum/0x123456789/events",
trackingCategory: "test-category",
},
};
function eventStub(eventName: string) {
return {
logIndex: 0,
eventName,
address: "0x",
args: [],
};
}
const txStub =
"0x9ef44ea701637e0f7c42666b6f99f4aad9aa6b9b51995cd22df1507288e51427";
export default meta;
type Story = StoryObj<typeof LatestEventsUI>;
export const Loading: Story = {
args: {
allEvents: [],
autoUpdate: true,
eventsHref: "/ethereum/0x123456789/events",
},
};
export const NoEvents: Story = {
args: {
allEvents: [],
autoUpdate: false,
eventsHref: "/ethereum/0x123456789/events",
},
};
export const WithEvents: Story = {
args: {
allEvents: [
{
transactionHash: txStub,
events: [eventStub("Transfer"), eventStub("Approval")],
},
{
transactionHash: txStub,
events: [eventStub("Mint")],
},
{
transactionHash: txStub,
events: [
eventStub("Transfer"),
eventStub("Burn"),
eventStub("Approval"),
],
},
],
autoUpdate: false,
eventsHref: "/ethereum/0x123456789/events",
},
};
export const LiveWithEvents: Story = {
args: {
allEvents: [
{
transactionHash: txStub,
events: [eventStub("Transfer"), eventStub("Approval")],
},
{
transactionHash: txStub,
events: [eventStub("Mint")],
},
{
transactionHash: txStub,
events: [
eventStub("Transfer"),
eventStub("Burn"),
eventStub("Approval"),
],
},
{
transactionHash: txStub,
events: [eventStub("RoleGranted"), eventStub("OwnershipTransferred")],
},
],
autoUpdate: true,
eventsHref: "/ethereum/0x123456789/events",
},
};
@@ -1,192 +1,157 @@
"use client";
import { CopyTextButton } from "@/components/ui/CopyTextButton";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import {
type InternalTransaction,
useActivity,
} from "@3rdweb-sdk/react/hooks/useActivity";
import {
Box,
ButtonGroup,
Flex,
List,
SimpleGrid,
Spinner,
Tooltip,
} from "@chakra-ui/react";
import { useClipboard } from "hooks/useClipboard";
import { CopyIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { ArrowRightIcon } from "lucide-react";
import Link from "next/link";
import type { ThirdwebContract } from "thirdweb";
import {
Button,
Card,
Heading,
LinkButton,
Text,
TrackedLink,
type TrackedLinkProps,
} from "tw-components";
import { shortenString } from "utils/usedapp-external";
interface LatestEventsProps {
trackingCategory: TrackedLinkProps["category"];
export function LatestEvents(props: {
trackingCategory: string;
contract: ThirdwebContract;
chainSlug: string;
}
export const LatestEvents: React.FC<LatestEventsProps> = ({
contract,
trackingCategory,
chainSlug,
}) => {
const [autoUpdate] = useState(true);
const eventsHref = `/${chainSlug}/${contract.address}/events`;
const allEvents = useActivity(contract, autoUpdate);
}) {
const autoUpdate = true;
const allEvents = useActivity(props.contract, autoUpdate);
return (
<Flex gap={6} flexDirection="column">
<Flex align="center" justify="space-between" w="full">
<h2 className="font-semibold text-2xl tracking-tight">Latest Events</h2>
<TrackedLink
category={trackingCategory}
label="view_all_events"
color="blue.400"
_light={{
color: "blue.600",
}}
gap={4}
href={eventsHref}
>
View all -&gt;
</TrackedLink>
</Flex>
<Card p={0} overflow="hidden">
<SimpleGrid
gap={2}
columns={9}
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
bg="blackAlpha.50"
_dark={{ bg: "whiteAlpha.50" }}
>
<Heading gridColumn="span 3" size="label.md">
Transaction Hash
</Heading>
<Heading gridColumn="span 6" size="label.md">
Events
</Heading>
</SimpleGrid>
<List overflow="auto">
{allEvents.length === 0 ? (
<div className="flex items-center justify-center py-4">
<Flex align="center" gap={2}>
{autoUpdate && <Spinner size="sm" speed="0.69s" />}
<Text size="body.md" fontStyle="italic">
{autoUpdate ? "listening for events" : "no events to show"}
</Text>
</Flex>
</div>
) : null}
<div>
{allEvents?.slice(0, 3).map((e) => (
<EventsFeedItem
key={e.transactionHash}
transaction={e}
contractAddress={contract.address}
chainSlug={chainSlug}
/>
))}
</div>
</List>
</Card>
</Flex>
<LatestEventsUI
allEvents={allEvents}
autoUpdate={autoUpdate}
eventsHref={`/${props.chainSlug}/${props.contract.address}/events`}
trackingCategory={props.trackingCategory}
/>
);
};
interface EventsFeedItemProps {
transaction: InternalTransaction;
contractAddress: string;
chainSlug: string;
}
const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
transaction,
contractAddress,
chainSlug,
}) => {
const { onCopy } = useClipboard(transaction.transactionHash);
const eventsHref = `/${chainSlug}/${contractAddress}/events`;
export function LatestEventsUI(props: {
allEvents: Pick<InternalTransaction, "transactionHash" | "events">[];
autoUpdate: boolean;
eventsHref: string;
trackingCategory: string;
}) {
const { allEvents, autoUpdate, eventsHref } = props;
return (
<div>
<SimpleGrid
columns={9}
gap={2}
as="li"
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
overflow="hidden"
alignItems="center"
_last={{ borderBottomWidth: 0 }}
>
<Box gridColumn="span 3">
<div className="flex flex-row items-center gap-3">
<Tooltip
p={0}
bg="transparent"
boxShadow="none"
label={
<Card py={2} px={4} bgColor="backgroundHighlight">
<Text size="label.sm">
Copy transaction hash to clipboard
</Text>
</Card>
}
<div className="rounded-lg border bg-card">
{/* header */}
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<div className="flex items-center gap-3">
<h2 className="font-semibold text-xl tracking-tight">
Latest Events
</h2>
{autoUpdate && (
<Badge
variant="outline"
className="hidden gap-2 bg-background text-sm lg:flex"
>
<Button
size="sm"
bg="transparent"
onClick={() => {
onCopy();
toast.info("Transaction hash copied.");
}}
>
<CopyIcon className="size-3" />
</Button>
</Tooltip>
<Text fontFamily="mono" noOfLines={1}>
{shortenString(transaction.transactionHash)}
</Text>
</div>
</Box>
<ButtonGroup
size="sm"
variant="outline"
gridColumn="span 6"
flexWrap="wrap"
gap={2}
spacing={0}
>
{transaction.events.slice(0, 2).map((e, idx) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<LinkButton key={idx} href={`${eventsHref}?event=${e.eventName}`}>
<Text color="whiteBg" fontWeight="600" isTruncated>
{e.eventName}
</Text>
</LinkButton>
))}
{transaction.events.length > 2 && (
<Button as="span" pointerEvents="none">
+ {transaction.events.length - 2}
</Button>
<span className="!pointer-events-auto relative flex size-2">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
<span className="relative inline-flex size-2 rounded-full bg-primary" />
</span>
Live
</Badge>
)}
</ButtonGroup>
</SimpleGrid>
</div>
<Button asChild variant="outline" size="sm" className="bg-background">
<TrackedLinkTW
category={props.trackingCategory}
label="view_all_events"
href={eventsHref}
className="flex items-center gap-2 text-muted-foreground text-sm"
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
{/* table */}
<TableContainer className="w-full border-none">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-52">Transaction Hash</TableHead>
<TableHead>Events</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allEvents.slice(0, 3).map((transaction) => {
return (
<TableRow key={transaction.transactionHash}>
<TableCell>
<CopyTextButton
textToShow={shortenString(transaction.transactionHash)}
textToCopy={transaction.transactionHash}
tooltip="Copy transaction hash"
copyIconPosition="left"
variant="ghost"
className="-translate-x-2 font-mono"
/>
</TableCell>
<TableCell>
<div className="flex w-max flex-wrap gap-2">
{transaction.events.slice(0, 3).map((e) => (
<Button
key={e.logIndex + e.address + e.eventName}
variant="outline"
size="sm"
className="h-auto rounded-full py-1"
asChild
>
<Link href={`${eventsHref}?event=${e.eventName}`}>
{e.eventName}
</Link>
</Button>
))}
{transaction.events.length > 3 && (
<Button
variant="outline"
size="sm"
className="h-auto rounded-full py-1 hover:bg-transparent"
asChild
>
<div>+ {transaction.events.length - 3}</div>
</Button>
)}
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
{allEvents.length === 0 && (
<div className="flex h-48 items-center justify-center text-sm">
{autoUpdate ? (
<div className="flex items-center gap-2">
<Spinner className="size-4" />
<p>Listening for events</p>
</div>
) : (
<p>No events found</p>
)}
</div>
)}
</div>
);
};
}
@@ -2,8 +2,10 @@
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { SkeletonContainer } from "@/components/ui/skeleton";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ArrowRightIcon } from "lucide-react";
import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import {
@@ -79,28 +81,37 @@ const DirectListingCards: React.FC<ListingCardsSectionProps> = ({
}
return (
<>
<div className="flex w-full items-center justify-between">
<h2 className="font-semibold text-2xl tracking-tight">
Direct Listing
<div className="rounded-lg border bg-card">
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">
Direct Listings
</h2>
<TrackedLinkTW
category={trackingCategory}
label="view_all_direct_listings"
className="text-link-foreground hover:text-foreground"
href={directListingsHref}
<Button
asChild
variant="outline"
size="sm"
className="gap-2 bg-background text-muted-foreground"
>
View all -&gt;
</TrackedLinkTW>
<TrackedLinkTW
category={trackingCategory}
label="view_all_direct_listings"
href={directListingsHref}
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
<ListingCards
listings={listings}
isPending={listingsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</>
<div className="p-6">
<ListingCards
listings={listings}
isPending={listingsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</div>
</div>
);
};
@@ -142,28 +153,36 @@ const EnglishAuctionCards: React.FC<ListingCardsSectionProps> = ({
}
return (
<>
<div className="flex w-full items-center justify-between">
<h2 className="font-semibold text-2xl tracking-tight">
<div className="rounded-lg border bg-card">
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">
English Auctions
</h2>
<TrackedLinkTW
category={trackingCategory}
label="view_all_english_auctions"
className="text-link-foreground hover:text-foreground"
href={englishAuctionsHref}
<Button
asChild
variant="outline"
size="sm"
className="gap-2 bg-background text-muted-foreground"
>
View all -&gt;
</TrackedLinkTW>
<TrackedLinkTW
category={trackingCategory}
label="view_all_english_auctions"
href={englishAuctionsHref}
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
<ListingCards
listings={auctions}
isPending={auctionsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</>
<div className="p-6">
<ListingCards
listings={auctions}
isPending={auctionsQuery.isPending}
trackingCategory={trackingCategory}
chainSlug={chainSlug}
contractAddress={contract.address}
/>
</div>
</div>
);
};
@@ -183,8 +202,7 @@ export const MarketplaceDetails: React.FC<MarketplaceDetailsVersionProps> = ({
chainSlug,
}) => {
return (
<div className="flex flex-col gap-6">
<h2 className="font-semibold text-2xl tracking-tight">Listings</h2>
<div className="flex flex-col gap-10">
<ListingStatsV3
contract={contract}
hasDirectListings={hasDirectListings}
@@ -262,12 +280,16 @@ const ListingCards: React.FC<ListingCardsProps> = ({
const englishAuctionsHref = `/${chainSlug}/${contractAddress}/english-auctions`;
return (
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 md:gap-6 max-sm:[&>*:nth-child(n+3)]:hidden">
<div className="grid grid-cols-1 gap-4 md:grid-cols-3 ">
{listings.map((listing, index) => (
<div
className="relative rounded-lg border border-border bg-card transition-colors hover:border-active-border"
className="group hover:-translate-y-0.5 relative flex h-full cursor-pointer flex-col rounded-lg bg-background duration-200 hover:scale-[1.01]"
key={`${listing.creatorAddress}-${index}`}
>
{/* border */}
<div className="absolute inset-0 rounded-lg border border-border" />
{/* image */}
<div className="relative aspect-square w-full overflow-hidden rounded-lg">
{/* Image */}
<SkeletonContainer
@@ -324,21 +346,23 @@ const ListingCards: React.FC<ListingCardsProps> = ({
/>
)}
{/* seller */}
<SkeletonContainer
loadedData={isPending ? undefined : listing.creatorAddress}
skeletonData={listing.creatorAddress}
className="mt-4"
className="mt-4 border-t pt-4"
render={(v) => (
<div>
<p className="text-muted-foreground text-sm">Seller</p>
<p className="text-muted-foreground text-xs">Seller</p>
<WalletAddress
className="relative z-[1] self-start"
className="relative z-[1] self-start text-xs"
address={v}
/>
</div>
)}
/>
{/* price */}
{!isPending && (
<Badge
variant="outline"
@@ -1,27 +1,27 @@
"use client";
import { Flex, useBreakpointValue } from "@chakra-ui/react";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ArrowRightIcon } from "lucide-react";
import type { ThirdwebContract } from "thirdweb";
import * as ERC721 from "thirdweb/extensions/erc721";
import * as ERC1155 from "thirdweb/extensions/erc1155";
import { useReadContract } from "thirdweb/react";
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
import { NFTCards } from "../../_components/NFTCards";
interface NFTDetailsProps {
type NFTDetailsProps = {
contract: ThirdwebContract;
trackingCategory: TrackedLinkProps["category"];
trackingCategory: string;
isErc721: boolean;
chainSlug: string;
}
};
export const NFTDetails: React.FC<NFTDetailsProps> = ({
export function NFTDetails({
contract,
trackingCategory,
isErc721,
chainSlug,
}) => {
const isMobile = useBreakpointValue({ base: true, md: false });
}: NFTDetailsProps) {
const nftsHref = `/${chainSlug}/${contract.address}/nfts`;
const nftQuery = useReadContract(
@@ -36,34 +36,37 @@ export const NFTDetails: React.FC<NFTDetailsProps> = ({
const displayableNFTs =
nftQuery.data
?.filter((token) => token.metadata.image || token.metadata.animation_url)
.slice(0, isMobile ? 2 : 3) || [];
.slice(0, 3) || [];
return displayableNFTs.length === 0 && !nftQuery.isPending ? null : (
<Flex direction="column" gap={{ base: 3, md: 6 }}>
<Flex align="center" justify="space-between" w="full">
<h2 className="font-semibold text-2xl tracking-tight">NFT Details</h2>
<TrackedLink
category={trackingCategory}
label="view_all_nfts"
color="blue.400"
_light={{
color: "blue.600",
}}
gap={4}
href={nftsHref}
>
View all -&gt;
</TrackedLink>
</Flex>
<NFTCards
nfts={displayableNFTs.map((t) => ({
...t,
contractAddress: contract.address,
chainId: contract.chain.id,
}))}
trackingCategory={trackingCategory}
isPending={nftQuery.isPending}
/>
</Flex>
<div className="rounded-lg border bg-card">
{/* header */}
<div className="flex items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">NFTs</h2>
<Button variant="outline" asChild className="bg-background" size="sm">
<TrackedLinkTW
category={trackingCategory}
label="view_all_nfts"
href={nftsHref}
className="text-muted-foreground"
>
View all <ArrowRightIcon className="ml-2 size-4 " />
</TrackedLinkTW>
</Button>
</div>
{/* cards */}
<div className="p-6">
<NFTCards
nfts={displayableNFTs.map((t) => ({
...t,
contractAddress: contract.address,
chainId: contract.chain.id,
}))}
trackingCategory={trackingCategory}
isPending={nftQuery.isPending}
/>
</div>
</div>
);
};
}
@@ -0,0 +1,84 @@
import type { Meta, StoryObj } from "@storybook/react";
import { ThirdwebProvider } from "thirdweb/react";
import { PermissionsTableUI } from "./PermissionsTable";
const meta: Meta<typeof PermissionsTableUI> = {
title: "Contracts/Overview/PermissionsTable",
component: PermissionsTableUI,
decorators: [
(Story) => (
<ThirdwebProvider>
<div className="container max-w-4xl py-10">
<Story />
</div>
</ThirdwebProvider>
),
],
args: {
viewMoreLink: "#",
trackingCategory: "test-category",
members: [],
isPending: false,
},
};
export default meta;
type Story = StoryObj<typeof PermissionsTableUI>;
export const Loading: Story = {
args: {
isPending: true,
members: [],
},
};
export const NoMembers: Story = {
args: {
isPending: false,
members: [],
},
};
export const OneMember: Story = {
args: {
isPending: false,
members: [
{
member: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
roles: ["admin"],
},
],
},
};
export const FiveMembers: Story = {
args: {
isPending: false,
members: [
{
member: "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37",
roles: ["admin", "minter"],
},
{
member: "0x83Dd93fA5D8343094f850f90B3fb90088C1bB425",
roles: ["transfer"],
},
{
member: "0x1f4202eD5c33d229bCda7B9B6AB3B57caDf423e6",
roles: ["admin", "transfer", "minter"],
},
{
member: "0x6844F0056d4C84CA2a894Fcf85473d75d8252b24",
roles: [
"admin",
"minter",
"transfer",
"metadata",
"burner",
"manager",
"lister",
],
},
],
},
};
@@ -1,36 +1,34 @@
"use client";
import { ToolTipLabel } from "@/components/ui/tooltip";
import { Box, Flex, List, SimpleGrid, Tag } from "@chakra-ui/react";
import { WalletAddress } from "@/components/blocks/wallet-address";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { getAllRoleMembers } from "contract-ui/hooks/permissions";
import { useClipboard } from "hooks/useClipboard";
import { CopyIcon } from "lucide-react";
import { ArrowRightIcon } from "lucide-react";
import { useMemo } from "react";
import { toast } from "sonner";
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
import { useReadContract } from "thirdweb/react";
import { Button } from "tw-components/button";
import { Card } from "tw-components/card";
import { Heading } from "tw-components/heading";
import { TrackedLink, type TrackedLinkProps } from "tw-components/link";
import { Text } from "tw-components/text";
import { shortenIfAddress } from "utils/usedapp-external";
interface PermissionsTableProps {
export function PermissionsTable(props: {
contract: ThirdwebContract;
trackingCategory: TrackedLinkProps["category"];
trackingCategory: string;
chainSlug: string;
}
export const PermissionsTable: React.FC<PermissionsTableProps> = ({
contract,
trackingCategory,
chainSlug,
}) => {
}) {
const allRoleMembers = useReadContract(getAllRoleMembers, {
contract,
contract: props.contract,
});
const permissionsHref = `/${chainSlug}/${contract.address}/permissions`;
const members = useMemo(() => {
return (
Object.entries(allRoleMembers.data || {}).reduce(
@@ -52,128 +50,82 @@ export const PermissionsTable: React.FC<PermissionsTableProps> = ({
}, [allRoleMembers.data]);
return (
<Flex gap={6} flexDirection="column">
<Flex align="center" justify="space-between" w="full">
<h2 className="font-semibold text-2xl tracking-tight">Permissions</h2>
<TrackedLink
category={trackingCategory}
label="view_all_permissions"
color="blue.400"
_light={{
color: "blue.600",
}}
gap={4}
href={permissionsHref}
>
View all -&gt;
</TrackedLink>
</Flex>
{contract && (
<Card p={0} overflow="hidden">
<SimpleGrid
gap={2}
columns={9}
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
bg="blackAlpha.50"
_dark={{ bg: "whiteAlpha.50" }}
>
<Heading gridColumn="span 3" size="label.md">
Member
</Heading>
<Heading gridColumn="span 6" size="label.md">
Roles
</Heading>
</SimpleGrid>
<List overflow="auto">
{members.length === 0 && (
<div className="flex items-center justify-center py-4">
<Flex align="center" gap={2}>
<Text size="body.md" fontStyle="italic">
{allRoleMembers.isPending
? "loading permissions"
: "no permissions found"}
</Text>
</Flex>
</div>
)}
<div>
{members.map((e) => (
<PermissionsItem key={e.member} data={e} />
))}
</div>
</List>
</Card>
)}
</Flex>
<PermissionsTableUI
members={members}
isPending={allRoleMembers.isPending}
viewMoreLink={`/${props.chainSlug}/${props.contract.address}/permissions`}
trackingCategory={props.trackingCategory}
/>
);
};
interface PermissionsItemProps {
data: { member: string; roles: string[] };
}
const PermissionsItem: React.FC<PermissionsItemProps> = ({ data }) => {
const { onCopy } = useClipboard(data.member);
export function PermissionsTableUI(props: {
viewMoreLink: string;
trackingCategory: string;
members: { member: string; roles: string[] }[];
isPending: boolean;
}) {
return (
<div>
<SimpleGrid
columns={9}
gap={2}
as="li"
borderBottomWidth="1px"
borderColor="borderColor"
padding={4}
overflow="hidden"
alignItems="center"
_last={{ borderBottomWidth: 0 }}
>
<Box gridColumn="span 2">
<div className="flex flex-row items-center gap-3">
<ToolTipLabel label="Copy address to clipboard">
<Button
size="sm"
bg="transparent"
onClick={() => {
onCopy();
toast.info("Address copied.");
}}
>
<CopyIcon className="size-3" />
</Button>
</ToolTipLabel>
<Text fontFamily="mono" noOfLines={1}>
{shortenIfAddress(data.member)}
</Text>
</div>
</Box>
<div className="rounded-lg border bg-card">
{/* header */}
<div className="flex w-full items-center justify-between border-b px-6 py-4">
<h2 className="font-semibold text-xl tracking-tight">Permissions</h2>
<Button asChild variant="outline" size="sm" className="bg-background">
<TrackedLinkTW
category={props.trackingCategory}
label="view_all_permissions"
href={props.viewMoreLink}
className="flex items-center gap-2 text-muted-foreground text-sm"
>
View all <ArrowRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
</div>
<Box gridColumn="span 1" />
{/* table */}
<TableContainer className="w-full border-none">
<Table>
<TableHeader>
<TableRow>
<TableHead>Member</TableHead>
<TableHead>Roles</TableHead>
</TableRow>
</TableHeader>
<Flex gridColumn="span 6" flexWrap="wrap" gap={2}>
{data.roles.slice(0, 3).map((role) => (
<Tag key={role}>
<Text size="body.md" fontWeight="medium">
{role}
</Text>
</Tag>
))}
{data.roles.length > 3 && (
<Tag
border="2px solid"
borderColor="var(--badge-bg)"
bg="transparent"
>
<Text size="body.md" fontWeight="medium">
+ {data.roles.length - 3}
</Text>
</Tag>
<TableBody>
{props.members.map((data) => (
<TableRow key={data.member}>
<TableCell>
<WalletAddress address={data.member} />
</TableCell>
<TableCell>
<div className="flex w-max flex-wrap gap-2">
{data.roles.map((role) => (
<Badge
variant="outline"
key={role}
className="bg-background py-1 font-normal text-sm capitalize"
>
{role}
</Badge>
))}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{props.members.length === 0 && (
<div className="flex h-48 items-center justify-center text-sm">
{props.isPending ? (
<Spinner className="size-8" />
) : (
<p>No Permissions</p>
)}
</Flex>
</SimpleGrid>
</div>
)}
</div>
);
};
}
@@ -1,17 +0,0 @@
import type { ThirdwebContract } from "thirdweb";
import { TokenSupply } from "../../tokens/components/supply";
interface TokenDetailsProps {
contract: ThirdwebContract;
}
export const TokenDetails: React.FC<TokenDetailsProps> = ({ contract }) => {
return (
<div className="flex flex-col gap-6">
<div className="flex w-full flex-row items-center justify-between">
<h2 className="font-semibold text-2xl tracking-tight">Token Details</h2>
</div>
<TokenSupply contract={contract} />
</div>
);
};
@@ -2,7 +2,7 @@ import { useMemo } from "react";
import type { ThirdwebContract } from "thirdweb";
import { totalAuctions, totalListings } from "thirdweb/extensions/marketplace";
import { useReadContract } from "thirdweb/react";
import { StatCard } from "./stat-card";
import { Stat } from "./stat-card";
const TotalListingsStat: React.FC<{ contract: ThirdwebContract }> = ({
contract,
@@ -19,7 +19,7 @@ const TotalListingsStat: React.FC<{ contract: ThirdwebContract }> = ({
);
return (
<StatCard
<Stat
value={combinedListingCount.toString()}
label="Total Listings"
isPending={
@@ -37,7 +37,7 @@ const DirectListingsStat: React.FC<{ contract: ThirdwebContract }> = ({
});
return (
<StatCard
<Stat
value={(directListingsQuery.data || 0n).toString()}
isPending={directListingsQuery.isPending}
label="Direct Listings"
@@ -53,7 +53,7 @@ const EnglishAuctionsStat: React.FC<{ contract: ThirdwebContract }> = ({
});
return (
<StatCard
<Stat
value={(englishAuctionsQuery.data || 0n).toString()}
isPending={englishAuctionsQuery.isPending}
label="English Auctions"
@@ -73,12 +73,17 @@ export const ListingStatsV3: React.FC<ListingStatsV3Props> = ({
hasEnglishAuctions,
}) => {
return (
<div className="flex flex-row gap-3 md:gap-6 [&>*]:grow">
{hasDirectListings && hasEnglishAuctions && contract && (
<TotalListingsStat contract={contract} />
)}
{hasDirectListings && <DirectListingsStat contract={contract} />}
{hasEnglishAuctions && <EnglishAuctionsStat contract={contract} />}
<div className="rounded-lg border bg-card">
<h2 className="border-b px-6 py-5 font-semibold text-xl tracking-tight">
Listing Details
</h2>
<div className="flex flex-col gap-4 p-6 lg:flex-row [&>*]:grow">
{hasDirectListings && hasEnglishAuctions && contract && (
<TotalListingsStat contract={contract} />
)}
{hasDirectListings && <DirectListingsStat contract={contract} />}
{hasEnglishAuctions && <EnglishAuctionsStat contract={contract} />}
</div>
</div>
);
};
@@ -1,17 +1,34 @@
import { SkeletonContainer } from "@/components/ui/skeleton";
export function StatCard(props: {
value: string;
isPending: boolean;
label: string;
}) {
export function Stat({
label,
value,
isPending,
}: { label: string; value: string; isPending: boolean }) {
return (
<dl className="block rounded-lg border border-border bg-card p-4">
<dt className="mb-1.5 text-sm md:text-base">{props.label}</dt>
<dl className="grow">
<dt className="text-muted-foreground text-sm">{label}</dt>
<SkeletonContainer
loadedData={props.isPending ? undefined : props.value}
skeletonData={"0000"}
render={(v) => <dd className="truncate font-semibold text-xl">{v}</dd>}
skeletonData={"000000"}
loadedData={isPending ? undefined : value}
render={(v) => <dd className="font-medium text-lg">{v}</dd>}
/>
</dl>
);
}
export function StatCard({
label,
value,
isPending,
}: { label: string; value: string; isPending: boolean }) {
return (
<dl className="grow rounded-lg border bg-card p-4">
<dt className="text-muted-foreground text-sm">{label}</dt>
<SkeletonContainer
skeletonData={"000000"}
loadedData={isPending ? undefined : value}
render={(v) => <dd className="font-medium text-lg">{v}</dd>}
/>
</dl>
);
@@ -1,7 +1,7 @@
import { Flex, SimpleGrid, Spinner } from "@chakra-ui/react";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { TrackedUnderlineLink } from "@/components/ui/tracked-link";
import type { ExtensionDetectedState } from "components/buttons/ExtensionDetectedState";
import { ExternalLinkIcon, Grid2x2XIcon } from "lucide-react";
import { Heading, Text, TrackedLink } from "tw-components";
import { Grid2x2XIcon } from "lucide-react";
const settingTypeMap = {
metadata: {
@@ -26,15 +26,13 @@ const settingTypeMap = {
},
} as const;
interface SettingDetectedStateProps {
type: keyof typeof settingTypeMap;
detectedState: ExtensionDetectedState;
}
export const SettingDetectedState: React.FC<SettingDetectedStateProps> = ({
export function SettingDetectedState({
type,
detectedState,
}) => {
}: {
type: keyof typeof settingTypeMap;
detectedState: ExtensionDetectedState;
}) {
if (detectedState === "enabled") {
return null;
}
@@ -42,45 +40,32 @@ export const SettingDetectedState: React.FC<SettingDetectedStateProps> = ({
const metadata = settingTypeMap[type];
return (
<SimpleGrid
position="absolute"
top={0}
left={0}
bottom={0}
right={0}
zIndex={1}
placeItems="center"
px={{ base: 6, md: 0 }}
borderRadius="md"
className="bg-background"
>
<div className="absolute inset-0 z-10 grid place-items-center bg-card p-6">
{detectedState === "loading" ? (
<Spinner size="sm" />
<Spinner className="size-4" />
) : (
<Flex flexDir="column" gap={3}>
<Flex align="center" gap={2}>
<div className="flex flex-col items-center text-center">
<div className="mb-2 flex size-10 items-center justify-center rounded-full border bg-background">
<Grid2x2XIcon className="size-4 text-red-500" />
<Heading size="subtitle.md">Missing extension</Heading>
</Flex>
<Text>
This contract does not implement the required extension for{" "}
<strong>{metadata.name}</strong>.
</Text>
<TrackedLink
category="contract-settings"
label="metadata-extension"
href={metadata.portalLink}
isExternal
display="flex"
flexDir="row"
alignItems="center"
gap={2}
>
Learn how to enable this extension
<ExternalLinkIcon className="size-4" />
</TrackedLink>
</Flex>
</div>
<div>
<h3 className="mb-2 font-semibold text-lg">Missing extension</h3>
<p className="mb-0.5 text-muted-foreground text-sm">
This contract does not implement the required extension for{" "}
<span className="text-foreground">{metadata.name}</span>
</p>
<TrackedUnderlineLink
category="contract-settings"
label="metadata-extension"
href={metadata.portalLink}
target="_blank"
className="text-muted-foreground text-sm"
>
Learn how to enable this extension
</TrackedUnderlineLink>
</div>
</div>
)}
</SimpleGrid>
</div>
);
};
}
@@ -143,7 +143,7 @@ export const SettingsMetadata = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="metadata" detectedState={detectedState} />
<Flex
as="form"
@@ -76,7 +76,7 @@ export const SettingsPlatformFees = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="platformFee" detectedState={detectedState} />
<Flex
as="form"
@@ -68,7 +68,7 @@ export const SettingsPrimarySale = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="primarySale" detectedState={detectedState} />
<Flex
as="form"
@@ -89,7 +89,7 @@ export const SettingsRoyalties = ({
);
return (
<Card p={0} position="relative">
<Card p={0} position="relative" overflow="hidden">
<SettingDetectedState type="royalties" detectedState={detectedState} />
<Flex
as="form"
@@ -1,41 +1,36 @@
"use client";
import { Badge } from "@/components/ui/badge";
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
import { Spinner } from "@/components/ui/Spinner/Spinner";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi";
import {
Divider,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalHeader,
ModalOverlay,
Spinner,
useDisclosure,
} from "@chakra-ui/react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { SourcesPanel } from "components/contract-components/shared/sources-panel";
import { useContractSources } from "contract-ui/hooks/useContractSources";
import { CircleCheckIcon, CircleXIcon } from "lucide-react";
import { useMemo, useState } from "react";
import {
CircleCheckIcon,
CircleXIcon,
RefreshCcwIcon,
ShieldCheckIcon,
} from "lucide-react";
import { useMemo } from "react";
import { toast } from "sonner";
import type { ThirdwebContract } from "thirdweb";
import { Heading } from "tw-components";
interface ContractSourcesPageProps {
contract: ThirdwebContract;
}
interface VerificationResult {
type VerificationResult = {
explorerUrl: string;
success: boolean;
alreadyVerified: boolean;
error?: string;
}
};
export async function verifyContract(contract: ThirdwebContract) {
try {
@@ -63,111 +58,80 @@ export async function verifyContract(contract: ThirdwebContract) {
}
}
interface ConnectorModalProps {
isOpen: boolean;
onClose: () => void;
function VerifyContractModalContent({
contract,
}: {
contract: ThirdwebContract;
}
const VerifyContractModal: React.FC<
ConnectorModalProps & { resetSignal: number }
> = ({ isOpen, onClose, contract, resetSignal }) => {
}) {
const verifyQuery = useQuery({
queryKey: [
"verify-contract",
contract.chain.id,
contract.address,
resetSignal,
],
queryKey: ["verify-contract", contract.chain.id, contract.address],
queryFn: () => verifyContract(contract),
enabled: isOpen,
});
return (
<Modal isOpen={isOpen} onClose={onClose} isCentered>
<ModalOverlay />
<ModalContent
className="!bg-background rounded-lg border border-border"
pb={2}
mx={{ base: 4, md: 0 }}
>
<ModalHeader>
<Flex gap={2} align="center">
<Heading size="subtitle.md">Contract Verification</Heading>
<Badge>beta</Badge>
</Flex>
</ModalHeader>
<ModalCloseButton mt={2} />
<Divider mb={4} />
<ModalBody py={4}>
<Flex flexDir="column">
{verifyQuery.isPending && (
<Flex gap={2} align="center">
<Spinner color="purple.500" size="sm" />
<Heading size="label.md">Verifying...</Heading>
</Flex>
)}
{verifyQuery?.error ? (
<Flex gap={2} align="center">
<CircleXIcon className="size-4 text-red-600" />
<Heading size="label.md">
{verifyQuery?.error.toString()}
</Heading>
</Flex>
) : null}
<div className="flex flex-col p-6">
{verifyQuery.isPending && (
<div className="flex min-h-24 items-center justify-center">
<div className="flex items-center gap-2">
<Spinner className="size-4" />
<p className="font-medium text-sm">Verifying</p>
</div>
</div>
)}
{verifyQuery.data?.results
? verifyQuery.data?.results.map(
(result: VerificationResult, index: number) => (
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
<Flex key={index} gap={2} align="center" mb={4}>
{result.success && (
<>
<CircleCheckIcon className="size-4 text-green-600" />
{result.alreadyVerified && (
<Heading size="label.md">
{result.explorerUrl}: Already verified
</Heading>
)}
{!result.alreadyVerified && (
<Heading size="label.md">
{result.explorerUrl}: Verification successful
</Heading>
)}
</>
)}
{!result.success && (
<>
<CircleXIcon className="size-4 text-red-600" />
<Heading size="label.md">
{`${result.explorerUrl}: Verification failed`}
</Heading>
</>
)}
</Flex>
),
)
: null}
</Flex>
</ModalBody>
</ModalContent>
</Modal>
{verifyQuery?.error ? (
<div className="flex min-h-24 items-center justify-center">
<div className="flex items-center gap-2">
<CircleXIcon className="size-4 text-red-600" />
<p className="font-medium text-sm">
{verifyQuery?.error.toString()}
</p>
</div>
</div>
) : null}
{verifyQuery.data?.results ? (
<div className="flex flex-col gap-2">
{verifyQuery.data.results.map(
(result: VerificationResult, index: number) => (
// biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<div key={index} className="flex items-center gap-2">
{result.success && (
<>
<CircleCheckIcon className="size-4 text-green-600" />
{result.alreadyVerified && (
<p className="font-medium text-sm">
{result.explorerUrl}: Already verified
</p>
)}
{!result.alreadyVerified && (
<p className="font-medium text-sm">
{result.explorerUrl}: Verification successful
</p>
)}
</>
)}
{!result.success && (
<>
<CircleXIcon className="size-4 text-red-600" />
<p className="font-medium text-sm">
{`${result.explorerUrl}: Verification failed`}
</p>
</>
)}
</div>
),
)}
</div>
) : null}
</div>
);
};
}
export const ContractSourcesPage: React.FC<ContractSourcesPageProps> = ({
export function ContractSourcesPage({
contract,
}) => {
const [resetSignal, setResetSignal] = useState(0);
const { isOpen, onOpen, onClose } = useDisclosure();
const handleClose = () => {
onClose();
// Increment to reset the query in the child component
setResetSignal((prev: number) => prev + 1);
};
}: { contract: ThirdwebContract }) {
const contractSourcesQuery = useContractSources(contract);
const abiQuery = useResolveContractAbi(contract);
@@ -187,44 +151,49 @@ export const ContractSourcesPage: React.FC<ContractSourcesPageProps> = ({
.reverse();
}, [contractSourcesQuery.data]);
if (!contractSourcesQuery || contractSourcesQuery?.isPending) {
return (
<Flex direction="row" align="center" gap={2}>
<Spinner color="purple.500" size="xs" />
<Heading size="title.sm">Loading...</Heading>
</Flex>
);
if (!contractSourcesQuery || contractSourcesQuery.isPending) {
return <GenericLoadingPage />;
}
return (
<>
<VerifyContractModal
isOpen={isOpen}
onClose={() => handleClose()}
contract={contract}
resetSignal={resetSignal}
/>
<Flex direction="column" gap={8}>
<Flex direction="row" alignItems="center" gap={2}>
<Heading size="title.sm" flex={1}>
Sources
</Heading>
<div>
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div>
<h2 className="font-semibold text-2xl tracking-tight">Sources</h2>
<p className="text-muted-foreground text-sm">
View ABI and source code for the contract
</p>
</div>
<div className="flex items-center gap-3">
<RefreshContractMetadataButton
chainId={contract.chain.id}
contractAddress={contract.address}
/>
<Button variant="primary" onClick={onOpen}>
Verify contract
</Button>
</Flex>
<Card>
<SourcesPanel sources={sources} abi={abiQuery.data} />
</Card>
</Flex>
</>
<Dialog>
<DialogTrigger asChild>
<Button className="gap-2" size="sm">
<ShieldCheckIcon className="size-4" />
Verify contract
</Button>
</DialogTrigger>
<DialogContent className="gap-0 overflow-hidden p-0">
<DialogHeader className="border-b p-6">
<DialogTitle>Verify Contract</DialogTitle>
</DialogHeader>
<VerifyContractModalContent contract={contract} />
</DialogContent>
</Dialog>
</div>
</div>
<div className="h-4" />
<div className="rounded-lg border bg-card">
<SourcesPanel sources={sources} abi={abiQuery.data} />
</div>
</div>
);
};
}
function RefreshContractMetadataButton(props: {
chainId: number;
@@ -270,14 +239,19 @@ function RefreshContractMetadataButton(props: {
onClick={() => {
toast.promise(contractCacheMutation.mutateAsync(), {
duration: 5000,
loading: "Refreshing contract data...",
success: () => "Contract data refreshed!",
success: () => "Contract refreshed successfully",
error: (e) => e?.message || "Failed to refresh contract data.",
});
}}
className="w-[182px]"
size="sm"
className="gap-2 bg-card"
>
{contractCacheMutation.isPending ? <Spinner /> : "Refresh Contract Data"}
{contractCacheMutation.isPending ? (
<Spinner className="size-4" />
) : (
<RefreshCcwIcon className="size-4" />
)}
Refresh Contract <span className="max-sm:hidden"> Data </span>
</Button>
);
}
@@ -6,7 +6,7 @@ import { TokenAirdropButton } from "./components/airdrop-button";
import { TokenBurnButton } from "./components/burn-button";
import { TokenClaimButton } from "./components/claim-button";
import { TokenMintButton } from "./components/mint-button";
import { TokenSupply } from "./components/supply";
import { TokenDetailsCard } from "./components/supply";
import { TokenTransferButton } from "./components/transfer-button";
interface ContractTokenPageProps {
@@ -49,7 +49,7 @@ export const ContractTokensPage: React.FC<ContractTokenPageProps> = ({
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<Heading size="title.sm">Contract Tokens</Heading>
<h2 className="font-semibold text-2xl tracking-tight">Tokens</h2>
<div className="flex flex-col gap-3 md:flex-row">
{isClaimToSupported && (
<TokenClaimButton contract={contract} twAccount={twAccount} />
@@ -63,7 +63,7 @@ export const ContractTokensPage: React.FC<ContractTokenPageProps> = ({
</div>
</div>
<TokenSupply contract={contract} />
<TokenDetailsCard contract={contract} />
</div>
);
};
@@ -1,60 +1,46 @@
import {
Skeleton,
Stat,
StatHelpText,
StatLabel,
StatNumber,
} from "@chakra-ui/react";
import type { GetBalanceResult } from "thirdweb/extensions/erc20";
import { Card } from "tw-components";
import { Stat } from "../../overview/components/stat-card";
interface TokenSupplyLayoutProps {
isTokenSupplySuccess: boolean;
export function TokenDetailsCardUI(props: {
tokenSupply: GetBalanceResult | undefined;
isOwnedBalanceSuccess: boolean;
address: string | undefined;
isWalletConnected: boolean;
ownedBalance: GetBalanceResult | undefined;
}
export const TokenSupplyLayout: React.FC<TokenSupplyLayoutProps> = ({
isTokenSupplySuccess,
tokenSupply,
isOwnedBalanceSuccess,
address,
ownedBalance,
}) => {
}) {
const { tokenSupply, isWalletConnected, ownedBalance } = props;
return (
<div className="flex flex-row gap-3 md:gap-6">
<Card as={Stat}>
<StatLabel mb={{ base: 1, md: 0 }}>Total Supply</StatLabel>
<Skeleton isLoaded={isTokenSupplySuccess}>
<StatNumber>
{tokenSupply?.displayValue} {tokenSupply?.symbol}
</StatNumber>
</Skeleton>
</Card>
<Card as={Stat}>
<StatLabel mb={{ base: 1, md: 0 }}>Owned by you</StatLabel>
<Skeleton isLoaded={isOwnedBalanceSuccess || !address}>
<StatNumber>
{address ? (
<>
{ownedBalance?.displayValue} {ownedBalance?.symbol}
</>
) : (
<StatHelpText as="span">
Connect your wallet to see your balance
</StatHelpText>
)}
</StatNumber>
</Skeleton>
</Card>
<Card as={Stat}>
<StatLabel mb={{ base: 1, md: 0 }}>Decimals</StatLabel>
<Skeleton isLoaded={isTokenSupplySuccess}>
<StatNumber>{tokenSupply?.decimals}</StatNumber>
</Skeleton>
</Card>
<div className="rounded-lg border bg-card">
<h2 className="border-b p-6 py-5 font-semibold text-xl tracking-tight">
Token Details
</h2>
<div className="flex flex-col gap-5 p-6 lg:flex-row">
<Stat
label="Total Supply"
isPending={!tokenSupply}
value={
tokenSupply
? `${tokenSupply.displayValue} ${tokenSupply.symbol}`
: ""
}
/>
{isWalletConnected && (
<Stat
label="Owned by you"
isPending={!ownedBalance}
value={
ownedBalance
? `${ownedBalance?.displayValue} ${ownedBalance?.symbol}`
: ""
}
/>
)}
<Stat
label="Decimals"
isPending={!tokenSupply}
value={tokenSupply ? tokenSupply?.decimals.toString() : ""}
/>
</div>
</div>
);
};
}
@@ -8,16 +8,18 @@ import {
totalSupply,
} from "thirdweb/extensions/erc20";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { TokenSupplyLayout } from "./supply-layout";
import { TokenDetailsCardUI } from "./supply-layout";
interface TokenBalancesProps {
contract: ThirdwebContract;
}
export const TokenSupply: React.FC<TokenBalancesProps> = ({ contract }) => {
export const TokenDetailsCard: React.FC<TokenBalancesProps> = ({
contract,
}) => {
const address = useActiveAccount()?.address;
const tokenBalanceQuery = useReadContract(getBalance, {
const ownedTokenBalanceQuery = useReadContract(getBalance, {
contract,
address: address || "",
queryOptions: { enabled: !!address },
@@ -25,7 +27,6 @@ export const TokenSupply: React.FC<TokenBalancesProps> = ({ contract }) => {
const tokenSupplyQuery = useReadContract(totalSupply, {
contract,
queryOptions: { enabled: !!address },
});
const tokenMetadataQuery = useReadContract(getCurrencyMetadata, { contract });
@@ -35,13 +36,7 @@ export const TokenSupply: React.FC<TokenBalancesProps> = ({ contract }) => {
tokenMetadataQuery.data === undefined ||
tokenSupplyQuery.data === undefined
) {
return {
value: 0n,
displayValue: "0.0",
symbol: "LOA",
decimals: 18,
name: "Loading...",
};
return undefined;
}
return {
@@ -55,14 +50,10 @@ export const TokenSupply: React.FC<TokenBalancesProps> = ({ contract }) => {
}, [tokenMetadataQuery.data, tokenSupplyQuery.data]);
return (
<TokenSupplyLayout
isTokenSupplySuccess={
tokenSupplyQuery.isSuccess && tokenMetadataQuery.isSuccess
}
<TokenDetailsCardUI
isWalletConnected={!!address}
tokenSupply={tokenSupply}
isOwnedBalanceSuccess={tokenBalanceQuery.isSuccess}
address={address}
ownedBalance={tokenBalanceQuery.data}
ownedBalance={ownedTokenBalanceQuery.data}
/>
);
};
@@ -14,7 +14,7 @@ export const products = [
id: "contracts",
icon: ContractIcon,
description: "Pre-built contracts & deployment tools",
link: "https://thirdweb.com/contracts",
link: "https://thirdweb.com/explore",
},
{
name: "Connect SDK",
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@@ -6,15 +6,16 @@ import zeroGCTA from "./temp-assets/0gCTA.png";
import zeroGBanner from "./temp-assets/0gLabsBanner.png";
import alephZeroBaner from "./temp-assets/AlephZeroBanner.jpg";
import alephZeroCTA from "./temp-assets/AlephZeroCTA.jpg";
import ancient8Banner from "./temp-assets/ancient8-banner.png";
import assetChainBanner from "./temp-assets/assetChainBanner.png";
import assetChainCTABG from "./temp-assets/assetChainCTABanner.png";
import cotiBanner from "./temp-assets/COTI_Banner.jpg";
import cotiCTA from "./temp-assets/COTI_CTA.jpg";
import hemiSepoliaBanner from "./temp-assets/HemiBanner.png";
import hemiSepoliaCTA from "./temp-assets/HemiCTA.png";
import ancient8Banner from "./temp-assets/ancient8-banner.png";
import appChainBanner from "./temp-assets/appChainBanner.jpg";
// TEMPORARY
import appchainCTA from "./temp-assets/appchainCTA.png";
import assetChainBanner from "./temp-assets/assetChainBanner.png";
import assetChainCTABG from "./temp-assets/assetChainCTABanner.png";
import baseBanner from "./temp-assets/base-banner.jpeg";
import superchainCTABG from "./temp-assets/cta-bg-superchain.png";
import xaiCTABg from "./temp-assets/cta-bg-xai-connect.png";
@@ -340,6 +341,30 @@ const chainMetaRecord = {
buttonText: "Learn More",
},
},
//COTI Mainnet
2632500: {
headerImgUrl: cotiBanner.src,
about:
"COTI is the fastest, lightest privacy layer in Web3, powered by the breakthrough cryptographic protocol Garbled Circuits. Build with privacy. Scale with speed.",
cta: {
backgroundImageUrl: cotiCTA.src,
title: "The Fastest, Lightest Privacy Layer in Web3",
buttonLink: "https://thirdweb.com/coti",
buttonText: "Learn More",
},
},
//COTI Testnet
7082400: {
headerImgUrl: cotiBanner.src,
about:
"COTI is the fastest, lightest privacy layer in Web3, powered by the breakthrough cryptographic protocol Garbled Circuits. Build with privacy. Scale with speed.",
cta: {
backgroundImageUrl: cotiCTA.src,
title: "The Fastest, Lightest Privacy Layer in Web3",
buttonLink: "https://thirdweb.com/coti",
buttonText: "Learn More",
},
},
//Donatuz
42026: {
headerImgUrl: thirdwebBanner.src,
@@ -33,6 +33,7 @@ export default async function DirectDeployPage(props: DirectDeployPageProps) {
/>
<DeployFormForUri
contractMetadata={metadata}
contractMetadataNoFee={metadata}
modules={null}
pathname={`/contracts/deploy/${params.compiler_uri}`}
/>
@@ -3,6 +3,7 @@ import {
fetchPublishedContractVersion,
fetchPublishedContractVersions,
} from "components/contract-components/fetch-contracts-with-versions";
import { ZERO_FEE_VERSIONS } from "constants/fee-config";
import { isAddress } from "thirdweb";
import { fetchDeployMetadata } from "thirdweb/contract";
import { resolveAddress } from "thirdweb/extensions/ens";
@@ -46,6 +47,10 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
publishedContractVersions.find((v) => v.version === props.version) ||
publishedContractVersions[0];
const publishedContractNoFee = publishedContractVersions.find(
(v) => v.version === ZERO_FEE_VERSIONS[v.name],
);
if (!publishedContract) {
return null;
}
@@ -53,22 +58,30 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
const moduleUris = modules
.filter((m) => m !== null && m !== undefined)
.map((m) => m.publishMetadataUri);
const [contractMetadata, ...fetchedModules] = await Promise.all([
fetchDeployMetadata({
client,
// force `ipfs://` prefix
uri: publishedContract.publishMetadataUri.startsWith("ipfs://")
? publishedContract.publishMetadataUri
: `ipfs://${publishedContract.publishMetadataUri}`,
}).catch(() => null),
...(moduleUris || []).map((uri) =>
const [contractMetadata, contractMetadataNoFee, ...fetchedModules] =
await Promise.all([
fetchDeployMetadata({
client,
// force `ipfs://` prefix
uri: uri.startsWith("ipfs://") ? uri : `ipfs://${uri}`,
uri: publishedContract.publishMetadataUri.startsWith("ipfs://")
? publishedContract.publishMetadataUri
: `ipfs://${publishedContract.publishMetadataUri}`,
}).catch(() => null),
),
]);
fetchDeployMetadata({
client,
// force `ipfs://` prefix
uri: publishedContractNoFee?.publishMetadataUri.startsWith("ipfs://")
? publishedContractNoFee.publishMetadataUri
: `ipfs://${publishedContractNoFee?.publishMetadataUri}`,
}).catch(() => null),
...(moduleUris || []).map((uri) =>
fetchDeployMetadata({
client,
// force `ipfs://` prefix
uri: uri.startsWith("ipfs://") ? uri : `ipfs://${uri}`,
}).catch(() => null),
),
]);
return (
<div className="mx-auto flex w-full max-w-[1000px] flex-col gap-8 pb-20">
@@ -79,6 +92,7 @@ export async function DeployFormForPublishInfo(props: PublishBasedDeployProps) {
/>
<DeployFormForUri
contractMetadata={contractMetadata}
contractMetadataNoFee={contractMetadataNoFee}
modules={fetchedModules.filter((m) => m !== null)}
pathname={`/${props.publisher}/${props.contract_id}${props.version ? `/${props.version}` : ""}/deploy`}
/>
@@ -8,12 +8,13 @@ import { loginRedirect } from "../../../login/loginRedirect";
type DeployFormForUriProps = {
contractMetadata: FetchDeployMetadataResult | null;
contractMetadataNoFee: FetchDeployMetadataResult | null;
modules: FetchDeployMetadataResult[] | null;
pathname: string;
};
export async function DeployFormForUri(props: DeployFormForUriProps) {
const { contractMetadata, modules, pathname } = props;
const { contractMetadata, contractMetadataNoFee, modules, pathname } = props;
if (!contractMetadata) {
return <div>Could not fetch metadata</div>;
@@ -46,6 +47,7 @@ export async function DeployFormForUri(props: DeployFormForUriProps) {
<ChakraProviderSetup>
<CustomContractForm
metadata={contractMetadata}
metadataNoFee={contractMetadataNoFee}
modules={modules?.filter((m) => m !== null)}
jwt={authToken}
teamsAndProjects={teamsAndProjects}
@@ -1,19 +1,21 @@
"use client";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ImportModal } from "components/contract-components/import-contract/modal";
import { DownloadIcon, PlusIcon } from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { useTrack } from "../../../hooks/analytics/useTrack";
export function DeployedContractsPageHeader(props: {
teamId: string;
projectId: string;
}) {
const [importModalOpen, setImportModalOpen] = useState(false);
const trackEvent = useTrack();
return (
<div>
<div className="border-b">
<ImportModal
isOpen={importModalOpen}
onClose={() => {
@@ -23,24 +25,38 @@ export function DeployedContractsPageHeader(props: {
projectId={props.projectId}
/>
<div className="flex flex-col gap-3 pb-6 lg:flex-row lg:items-center lg:justify-between">
<div className="container flex max-w-7xl flex-col gap-3 py-10 lg:flex-row lg:items-center lg:justify-between">
<div>
<h1 className="font-semibold text-3xl tracking-tight">Contracts</h1>
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
Contracts
</h1>
</div>
<div className="flex gap-3 [&>*]:grow">
<Button
className="gap-2 bg-card"
variant="outline"
onClick={() => setImportModalOpen(true)}
onClick={() => {
trackEvent({
action: "click",
category: "contracts",
label: "import-contract",
});
setImportModalOpen(true);
}}
>
<DownloadIcon className="size-4" />
Import contract
</Button>
<Button asChild className="gap-2">
<Link href="/explore">
<TrackedLinkTW
href="/explore"
category="contracts"
label="deploy-contract"
target="_blank"
>
<PlusIcon className="size-4" />
Deploy contract
</Link>
</TrackedLinkTW>
</Button>
</div>
</div>
@@ -0,0 +1,71 @@
"use client";
import { Button } from "@/components/ui/button";
import { TrackedLinkTW } from "@/components/ui/tracked-link";
import { ArrowUpRightIcon, DownloadIcon } from "lucide-react";
import { useState } from "react";
import { ImportModal } from "../../../../components/contract-components/import-contract/modal";
import { useTrack } from "../../../../hooks/analytics/useTrack";
export function DeployViaCLIOrImportCard(props: {
teamId: string;
projectId: string;
}) {
const trackEvent = useTrack();
const [importModalOpen, setImportModalOpen] = useState(false);
return (
<div className="rounded-lg border bg-card p-4 lg:p-6">
<ImportModal
isOpen={importModalOpen}
onClose={() => {
setImportModalOpen(false);
}}
teamId={props.teamId}
projectId={props.projectId}
/>
<h2 className="mb-0.5 font-semibold text-lg">
Already have a smart contract?
</h2>
<p className="max-w-2xl text-muted-foreground text-sm lg:text-base">
Import an already deployed contract or deploy a contract from source
code to easily manage permissions, upload assets, and interact with
contract functions
</p>
<div className="mt-6 flex flex-col gap-3 lg:flex-row">
<Button
variant="outline"
className="gap-2 bg-background lg:px-10"
asChild
>
<TrackedLinkTW
href="https://portal.thirdweb.com/contracts/deploy/overview"
target="_blank"
category="contracts-banner"
label="deploy-via-cli"
>
Deploy via CLI
<ArrowUpRightIcon className="size-4" />
</TrackedLinkTW>
</Button>
<Button
variant="outline"
className="gap-2 bg-background"
onClick={() => {
setImportModalOpen(true);
trackEvent({
category: "contracts-banner",
action: "click",
label: "import-contract",
});
}}
>
<DownloadIcon className="size-4" />
Import Contract
</Button>
</div>
</div>
);
}
@@ -3,7 +3,7 @@ import { ClientOnly } from "components/ClientOnly/ClientOnly";
import { Suspense } from "react";
import { ContractTable } from "../../../../components/contract-components/tables/contract-table";
import { DeployedContractsPageHeader } from "../DeployedContractsPageHeader";
import { GetStartedWithContractsDeploy } from "./GetStartedWithContractsDeploy";
import { DeployViaCLIOrImportCard } from "./DeployViaCLIOrImportCard";
import { getSortedDeployedContracts } from "./getSortedDeployedContracts";
export function DeployedContractsPage(props: {
@@ -17,10 +17,16 @@ export function DeployedContractsPage(props: {
teamId={props.teamId}
projectId={props.projectId}
/>
<div className="flex grow flex-col">
<div className="h-6" />
<div className="container flex max-w-7xl grow flex-col">
<Suspense fallback={<Loading />}>
<DeployedContractsPageAsync {...props} />
</Suspense>
<div className="h-8" />
<DeployViaCLIOrImportCard
teamId={props.teamId}
projectId={props.projectId}
/>
</div>
</div>
);
@@ -37,15 +43,6 @@ async function DeployedContractsPageAsync(props: {
authToken: props.authToken,
});
if (deployedContracts.length === 0) {
return (
<GetStartedWithContractsDeploy
teamId={props.teamId}
projectId={props.projectId}
/>
);
}
return (
<ClientOnly ssr={<Loading />}>
<ContractTable
@@ -1,139 +0,0 @@
"use client";
import { TabButtons } from "@/components/ui/tabs";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { ImportModal } from "components/contract-components/import-contract/modal";
import { StepsCard } from "components/dashboard/StepsCard";
import { useTrack } from "hooks/analytics/useTrack";
import { useMemo, useState } from "react";
export function GetStartedWithContractsDeploy(props: {
teamId: string;
projectId: string;
}) {
const steps = useMemo(
() => [
{
title: "Build, deploy or import a contract",
description:
"Choose between deploying your own contract or import an existing one.",
children: (
<DeployOptions teamId={props.teamId} projectId={props.projectId} />
),
completed: false, // because we only show this component if the user does not have any contracts
},
],
[props.teamId, props.projectId],
);
return (
<StepsCard
title="Get started with deploying contracts"
description="This guide will help you to start deploying contracts on-chain in just a few minutes."
steps={steps}
/>
);
}
type ContentItem = {
title: string;
description: string;
href?: string;
onClick?: () => void;
};
type TabId = "explore" | "import" | "build" | "deploy";
const DeployOptions = (props: {
teamId: string;
projectId: string;
}) => {
const [showImportModal, setShowImportModal] = useState(false);
const router = useDashboardRouter();
const trackEvent = useTrack();
const content: Record<TabId, ContentItem> = useMemo(
() => ({
explore: {
title: "Ready-to-deploy",
description:
"Pick from our library of ready-to-deploy contracts and deploy to any EVM chain in just 1-click.",
href: "/explore",
},
import: {
title: "Import",
description:
"Import an already deployed contract to build apps on top of contract using thirdweb tools.",
onClick: () => setShowImportModal(true),
},
build: {
title: "Build your own",
description:
"Get started with the Solidity SDK to create custom contracts specific to your use case.",
href: "/build",
},
deploy: {
title: "Deploy from source",
description:
"Deploy your contract by using our interactive CLI. (Supports Hardhat, Forge, Truffle, and more)",
href: "https://portal.thirdweb.com/cli",
},
}),
[],
);
const [activeTab, setActiveTab] = useState<TabId>("explore");
const activeTabContent = content[activeTab];
return (
<div className="mt-3">
<ImportModal
isOpen={showImportModal}
onClose={() => {
setShowImportModal(false);
}}
teamId={props.teamId}
projectId={props.projectId}
/>
<TabButtons
tabs={Object.entries(content).map(([key, value]) => ({
key,
name: value.title,
isActive: activeTab === key,
onClick: () => setActiveTab(key as TabId),
}))}
tabClassName="font-medium"
tabContainerClassName="gap-1"
/>
<button
className="mt-3 flex w-full cursor-pointer items-center gap-2 rounded-lg border border-border bg-card p-4 hover:border-active-border"
type="button"
onClick={() => {
activeTabContent.onClick?.();
if (activeTabContent.href) {
router.push(activeTabContent.href);
}
trackEvent({
category: "your_contracts",
action: "click",
label: "deploy_options",
type: activeTab,
href: null,
isExternal: false,
});
}}
>
<div>
<h4 className="text-start font-semibold text-lg">
{activeTabContent.title}
</h4>
<p className="text-start text-muted-foreground text-sm">
{activeTabContent.description}
</p>
</div>
</button>
</div>
);
};
@@ -10,7 +10,6 @@ import {
import { getThirdwebClient } from "@/constants/thirdweb.server";
import type { Meta, StoryObj } from "@storybook/react";
import { useState } from "react";
import { Toaster } from "sonner";
import { mobileViewport } from "../../../stories/utils";
import { AccountSettingsPageUI } from "./AccountSettingsPageUI";
@@ -147,7 +146,6 @@ function Variants() {
await new Promise((resolve) => setTimeout(resolve, 1000));
}}
/>
<Toaster richColors />
</div>
);
}
@@ -1,6 +1,5 @@
import type { LinkedWallet } from "@/api/linked-wallets";
import type { Meta, StoryObj } from "@storybook/react";
import { Toaster } from "sonner";
import { ThirdwebProvider } from "thirdweb/react";
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
import { LinkWalletUI } from "./LinkWalletUI";
@@ -91,8 +90,6 @@ function Variants() {
accountEmail="[email protected]"
/>
</BadgeContainer>
<Toaster richColors />
</div>
</ThirdwebProvider>
);
@@ -1,23 +1,45 @@
import type { Team } from "@/api/team";
import { Badge } from "@/components/ui/badge";
import { Badge, type BadgeProps } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
const teamPlanToBadgeVariant: Record<
Team["billingPlan"],
BadgeProps["variant"]
> = {
// gray
free: "secondary",
starter: "secondary",
// yellow
starter_legacy: "warning",
growth_legacy: "warning",
// green
accelerate: "success",
growth: "success",
scale: "success",
// blue
pro: "default",
};
function getTeamPlanBadgeLabel(plan: Team["billingPlan"]) {
if (plan === "growth_legacy") {
return "Growth - Legacy";
}
if (plan === "starter_legacy") {
return "Starter - Legacy";
}
return plan;
}
export function TeamPlanBadge(props: {
plan: Team["billingPlan"];
className?: string;
}) {
return (
<Badge
variant={
props.plan === "free" || props.plan === "starter"
? "secondary"
: props.plan === "growth"
? "success"
: "default"
}
variant={teamPlanToBadgeVariant[props.plan]}
className={cn("px-1.5 capitalize", props.className)}
>
{props.plan}
{getTeamPlanBadgeLabel(props.plan)}
</Badge>
);
}

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